# Survey form: HTML, React and Next.js code

> A short customer survey with single choice, multiple choice and a 0 to 10 recommendation score, stored as typed values you can export to CSV.

Updated 2026-10-24 · https://ikiform.com/forms/survey

## Copy the code

Every version below is generated from the same field definition. Replace `frm_xxxxxxxxxxxxxxxx` with your form id, or enter it in the **Form ID** box and the code updates.

**HTML** (`survey.html`)

```html
<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="role">Your role</label>
  <select id="role" name="role" required>
    <option value="">Choose one</option>
    <option value="engineering">Engineering</option>
    <option value="design">Design</option>
    <option value="product">Product</option>
    <option value="marketing">Marketing</option>
    <option value="other">Other</option>
  </select>
  <fieldset>
    <legend>How often do you use it?</legend>
    <label><input type="radio" name="frequency" value="daily" required /> Every day</label>
    <label><input type="radio" name="frequency" value="weekly" required /> Every week</label>
    <label><input type="radio" name="frequency" value="monthly" required /> Every month</label>
    <label><input type="radio" name="frequency" value="rarely" required /> Rarely</label>
  </fieldset>
  <fieldset>
    <legend>Which features do you use most? Pick up to three.</legend>
    <label><input type="checkbox" name="features[]" value="dashboard" /> Dashboard</label>
    <label><input type="checkbox" name="features[]" value="reports" /> Reports</label>
    <label><input type="checkbox" name="features[]" value="integrations" /> Integrations</label>
    <label><input type="checkbox" name="features[]" value="api" /> API</label>
    <label><input type="checkbox" name="features[]" value="mobile" /> Mobile app</label>
  </fieldset>
  <label for="recommend">How likely are you to recommend us, from 0 to 10?</label>
  <input id="recommend" name="recommend" type="number" min="0" max="10" step="1" required />
  <label for="improve">What should we improve?</label>
  <textarea id="improve" name="improve" rows="4" maxlength="4000"></textarea>
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Submit</button>
</form>
```

**React** (`SurveyForm.tsx`)

```tsx
import { submit } from "@ikiform/sdk";
import { type FormEvent, useState } from "react";

const FORM_ID = "frm_xxxxxxxxxxxxxxxx";

type Status = "idle" | "sending" | "sent" | "error";

export function SurveyForm() {
  const [status, setStatus] = useState<Status>("idle");
  const [errors, setErrors] = useState<Record<string, string[]>>({});
  const [message, setMessage] = useState("");

  async function onSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault();
    setStatus("sending");
    setErrors({});
    const result = await submit(FORM_ID, new FormData(event.currentTarget));
    if (result.ok) return setStatus("sent");
    setErrors(result.error.fields ?? {});
    setMessage(result.error.message);
    setStatus("error");
  }

  if (status === "sent") return <p>Thanks for taking the survey.</p>;

  return (
    <form onSubmit={onSubmit}>
      <label htmlFor="role">Your role</label>
      <select id="role" name="role" required>
        <option value="">Choose one</option>
        <option value="engineering">Engineering</option>
        <option value="design">Design</option>
        <option value="product">Product</option>
        <option value="marketing">Marketing</option>
        <option value="other">Other</option>
      </select>
      {errors.role ? <p>{errors.role[0]}</p> : null}
      <fieldset>
        <legend>How often do you use it?</legend>
        <label><input type="radio" name="frequency" value="daily" required /> Every day</label>
        <label><input type="radio" name="frequency" value="weekly" required /> Every week</label>
        <label><input type="radio" name="frequency" value="monthly" required /> Every month</label>
        <label><input type="radio" name="frequency" value="rarely" required /> Rarely</label>
      </fieldset>
      {errors.frequency ? <p>{errors.frequency[0]}</p> : null}
      <fieldset>
        <legend>Which features do you use most? Pick up to three.</legend>
        <label><input type="checkbox" name="features[]" value="dashboard" /> Dashboard</label>
        <label><input type="checkbox" name="features[]" value="reports" /> Reports</label>
        <label><input type="checkbox" name="features[]" value="integrations" /> Integrations</label>
        <label><input type="checkbox" name="features[]" value="api" /> API</label>
        <label><input type="checkbox" name="features[]" value="mobile" /> Mobile app</label>
      </fieldset>
      {errors.features ? <p>{errors.features[0]}</p> : null}
      <label htmlFor="recommend">How likely are you to recommend us, from 0 to 10?</label>
      <input id="recommend" name="recommend" type="number" min={0} max={10} step={1} required />
      {errors.recommend ? <p>{errors.recommend[0]}</p> : null}
      <label htmlFor="improve">What should we improve?</label>
      <textarea id="improve" name="improve" rows={4} maxLength={4000} />
      {errors.improve ? <p>{errors.improve[0]}</p> : null}
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {status === "error" && !Object.keys(errors).length ? <p role="alert">{message}</p> : null}
      <button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Submit"}</button>
    </form>
  );
}
```

**JavaScript** (`survey.js`)

```js
const form = document.querySelector('form[action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx"]');
const status = document.createElement("p");
status.setAttribute("role", "alert");
form.append(status);

const labels = {
  role: "Your role",
  frequency: "How often do you use it?",
  features: "Which features do you use most? Pick up to three.",
  recommend: "How likely are you to recommend us, from 0 to 10?",
  improve: "What should we improve?",
};

form.addEventListener("submit", async (event) => {
  event.preventDefault();
  const button = form.querySelector("button");
  button.disabled = true;
  status.textContent = "";
  try {
    const response = await fetch(form.action, {
      method: "POST",
      headers: { Accept: "application/json" },
      body: new FormData(form),
    });
    const result = await response.json();
    if (result.ok) {
      form.replaceWith("Thanks for taking the survey.");
      return;
    }
    const fields = Object.entries(result.error.fields ?? {});
    status.textContent = fields.length
      ? fields.map(([id, messages]) => `${labels[id] ?? id}: ${messages[0]}`).join(" ")
      : result.error.message;
  } catch {
    status.textContent = "Couldn't reach the server. Try again.";
  }
  button.disabled = false;
});
```

- **HTML** needs no JavaScript. After a successful post the browser is redirected to the form's redirect URL, or to Ikiform's hosted thank-you page.
- **React** uses the SDK: `npm install @ikiform/sdk`. Field errors from the server appear under each field.
- **JavaScript** enhances the HTML form with `fetch`, so the visitor stays on the page.
- The hidden `_gotcha` input is the honeypot. Leave it empty and hidden.

> If you set the form's spam check to **Ikiform**, the HTML form keeps working with no change and the server action skips the check because it sends a secret key. The React and JavaScript versions must get a token before each submit: call `challenge(FORM_ID)` from `@ikiform/sdk` and pass it to `submit` as `turnstileToken`, or add it to the `fetch` body as `cf-turnstile-response`. Without it the post is refused with `403 challenge_required`. See [the Next.js guide](https://ikiform.com/docs/guides/nextjs.md#client-human-check) and [Spam protection](https://ikiform.com/docs/spam.md#human-check).

## The fields

Paste this definition on the form's **Fields** tab in the dashboard. Ikiform then checks every post against it on the server, refuses bad data with `422 validation_failed`, refuses any field that isn't listed, and stores typed values.

| Field id | Type | Required | Accepts |
| --- | --- | --- | --- |
| `role` | `select` | Yes | One of: `engineering`, `design`, `product`, `marketing`, `other` |
| `frequency` | `radio` | Yes | One of: `daily`, `weekly`, `monthly`, `rarely` |
| `features` | `multi_select` | Yes | Any of: `dashboard`, `reports`, `integrations`, `api`, `mobile`, at most 3 |
| `recommend` | `number` | Yes | 0 to 10, whole numbers |
| `improve` | `textarea` | No | Up to 4,000 characters |

`Field definition`:

```json
{
  "version": 1,
  "title": "Customer survey",
  "fields": [
    {
      "id": "role",
      "type": "select",
      "label": "Your role",
      "required": true,
      "options": [
        {
          "value": "engineering",
          "label": "Engineering"
        },
        {
          "value": "design",
          "label": "Design"
        },
        {
          "value": "product",
          "label": "Product"
        },
        {
          "value": "marketing",
          "label": "Marketing"
        },
        {
          "value": "other",
          "label": "Other"
        }
      ]
    },
    {
      "id": "frequency",
      "type": "radio",
      "label": "How often do you use it?",
      "required": true,
      "options": [
        {
          "value": "daily",
          "label": "Every day"
        },
        {
          "value": "weekly",
          "label": "Every week"
        },
        {
          "value": "monthly",
          "label": "Every month"
        },
        {
          "value": "rarely",
          "label": "Rarely"
        }
      ]
    },
    {
      "id": "features",
      "type": "multi_select",
      "label": "Which features do you use most? Pick up to three.",
      "required": true,
      "options": [
        {
          "value": "dashboard",
          "label": "Dashboard"
        },
        {
          "value": "reports",
          "label": "Reports"
        },
        {
          "value": "integrations",
          "label": "Integrations"
        },
        {
          "value": "api",
          "label": "API"
        },
        {
          "value": "mobile",
          "label": "Mobile app"
        }
      ],
      "validation": {
        "max_items": 3
      }
    },
    {
      "id": "recommend",
      "type": "number",
      "label": "How likely are you to recommend us, from 0 to 10?",
      "required": true,
      "validation": {
        "min": 0,
        "max": 10,
        "integer": true
      }
    },
    {
      "id": "improve",
      "type": "textarea",
      "label": "What should we improve?",
      "rows": 4,
      "validation": {
        "max_length": 4000
      }
    }
  ],
  "settings": {
    "submit_label": "Submit",
    "success_message": "Thanks for taking the survey."
  }
}
```

## Next.js server action

To keep the form working without JavaScript in a Next.js app, post from a server action with a secret key. Create the key under **API keys** with the `submissions:write` scope and keep it out of the browser.

`.env.local`:

```bash
IKIFORM_SECRET_KEY=ik_sk_your_secret_key
```

`app/survey/actions.ts`:

```ts
"use server";

export interface SurveyFormState {
  ok: boolean;
  message: string;
  fields?: Record<string, string[]>;
  values?: Record<string, string[]>;
}

export async function sendSurveyForm(_previous: SurveyFormState, formData: FormData): Promise<SurveyFormState> {
  const body = new URLSearchParams();
  for (const [name, value] of formData) {
    if (typeof value === "string" && !name.startsWith("$ACTION")) body.append(name, value);
  }
  const response = await fetch("https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx", {
    method: "POST",
    headers: {
      Accept: "application/json",
      Authorization: `Bearer ${process.env.IKIFORM_SECRET_KEY}`,
    },
    body,
  });
  const result = await response.json();
  if (result.ok) return { ok: true, message: "Thanks for taking the survey." };
  const values = Object.fromEntries([...new Set(body.keys())].map((name) => [name, body.getAll(name)]));
  return { ok: false, message: result.error.message, fields: result.error.fields, values };
}
```

`app/survey/form.tsx`:

```tsx
"use client";

import { useActionState } from "react";
import { type SurveyFormState, sendSurveyForm } from "./actions";

const initial: SurveyFormState = { ok: false, message: "" };

export function SurveyForm() {
  const [state, action, pending] = useActionState(sendSurveyForm, initial);

  if (state.ok) return <p>{state.message}</p>;

  return (
    <form action={action}>
      <label htmlFor="role">Your role</label>
      <select id="role" name="role" required defaultValue={state.values?.role?.[0]}>
        <option value="">Choose one</option>
        <option value="engineering">Engineering</option>
        <option value="design">Design</option>
        <option value="product">Product</option>
        <option value="marketing">Marketing</option>
        <option value="other">Other</option>
      </select>
      {state.fields?.role ? <p>{state.fields.role[0]}</p> : null}
      <fieldset>
        <legend>How often do you use it?</legend>
        <label><input type="radio" name="frequency" value="daily" required defaultChecked={state.values?.frequency?.includes("daily")} /> Every day</label>
        <label><input type="radio" name="frequency" value="weekly" required defaultChecked={state.values?.frequency?.includes("weekly")} /> Every week</label>
        <label><input type="radio" name="frequency" value="monthly" required defaultChecked={state.values?.frequency?.includes("monthly")} /> Every month</label>
        <label><input type="radio" name="frequency" value="rarely" required defaultChecked={state.values?.frequency?.includes("rarely")} /> Rarely</label>
      </fieldset>
      {state.fields?.frequency ? <p>{state.fields.frequency[0]}</p> : null}
      <fieldset>
        <legend>Which features do you use most? Pick up to three.</legend>
        <label><input type="checkbox" name="features[]" value="dashboard" defaultChecked={state.values?.["features[]"]?.includes("dashboard")} /> Dashboard</label>
        <label><input type="checkbox" name="features[]" value="reports" defaultChecked={state.values?.["features[]"]?.includes("reports")} /> Reports</label>
        <label><input type="checkbox" name="features[]" value="integrations" defaultChecked={state.values?.["features[]"]?.includes("integrations")} /> Integrations</label>
        <label><input type="checkbox" name="features[]" value="api" defaultChecked={state.values?.["features[]"]?.includes("api")} /> API</label>
        <label><input type="checkbox" name="features[]" value="mobile" defaultChecked={state.values?.["features[]"]?.includes("mobile")} /> Mobile app</label>
      </fieldset>
      {state.fields?.features ? <p>{state.fields.features[0]}</p> : null}
      <label htmlFor="recommend">How likely are you to recommend us, from 0 to 10?</label>
      <input id="recommend" name="recommend" type="number" min={0} max={10} step={1} required defaultValue={state.values?.recommend?.[0]} />
      {state.fields?.recommend ? <p>{state.fields.recommend[0]}</p> : null}
      <label htmlFor="improve">What should we improve?</label>
      <textarea id="improve" name="improve" rows={4} maxLength={4000} defaultValue={state.values?.improve?.[0]} />
      {state.fields?.improve ? <p>{state.fields.improve[0]}</p> : null}
      <input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
      {state.message && !state.fields ? <p role="alert">{state.message}</p> : null}
      <button disabled={pending}>{pending ? "Sending" : "Submit"}</button>
    </form>
  );
}
```

> A server action posts from your server's IP, so the per-visitor rate limit counts every visitor together. Use the Relaxed or Custom [rate limit](https://ikiform.com/docs/rate-limits.md) for forms you send through a server.

## Set it up

1. Create a form in the [dashboard](https://ikiform.com/dashboard) and copy its id.
2. Paste the field definition on the **Fields** tab.
3. Add your site under **Allowed domains**, plus `localhost` while you develop.
4. Set the rate limit to **Standard, 5 a minute per visitor** if visitors post from the browser. Each person answers once. Standard is enough; tighten it to Custom only if you see repeat answers. With the server action, every post comes from your server's IP, so choose Relaxed or Custom instead.
5. Paste the code, send a test and check it arrives in the dashboard.

## Multiple choice

`features` is a `multi_select`. In HTML its checkboxes are named `features[]`, so even a single choice is stored as an array, like `["api"]`. Ikiform removes duplicates, refuses values that aren't listed, and enforces `max_items` on the server.

| Posted | Stored or refused |
| --- | --- |
| `features[]=api&features[]=reports` | `["api", "reports"]` |
| `features[]=api` | `["api"]` |
| Four features | `Choose at most 3 options` |
| Nothing | `Choose at least one option` |

## A 0 to 10 score

The `rating` type starts at 1, so a recommendation score from 0 to 10 uses a `number` field with `min: 0`, `max: 10` and `integer: true`. It is stored as a number. Group 9 and 10 as promoters and 0 to 6 as detractors when you analyse it.

## Reading the results

- Export responses as CSV from the dashboard, filtered by form and date. See [Export](https://ikiform.com/docs/guides/export.md).
- Or send each response to Google Sheets or Airtable as it arrives and chart it there.
- A survey can have up to 100 fields, but every extra question costs answers. Keep it to what you will act on.

> Responses aren't anonymous to Ikiform: each one records the user agent, origin, referrer and country. The visitor's IP is stored only as a hash and never returned by the API.

## Where to send it

Every submission can go to a signed [webhook](https://ikiform.com/docs/webhooks.md). These guides cover the tools that suit this form, with the relay code:

- [Send form submissions to Google Sheets](https://ikiform.com/blog/form-submissions-to-google-sheets.md)
- [Send form submissions to Airtable](https://ikiform.com/blog/form-submissions-to-airtable.md)
- [Send form submissions to a Notion database](https://ikiform.com/blog/form-submissions-to-notion.md)
- [Send form submissions to n8n](https://ikiform.com/blog/form-submissions-to-n8n.md)

## Questions

### Can I run an NPS survey with Ikiform?

Yes. Use a number field with min 0, max 10 and integer true for the score. The rating type starts at 1, so it doesn't fit a 0 to 10 scale.

### How are multiple choice answers stored?

As an array of the chosen option values, with duplicates removed. Only listed values are accepted, and min_items and max_items are checked on the server.

### How do I get survey results out?

Export CSV from the dashboard, list submissions with the REST API, or stream each response to a spreadsheet or database by webhook.
