Forms

3 fields

Feedback form

A short feedback form with a 1 to 5 rating and an optional comment, for product pages, docs and in-app widgets.

Form ID

Preview

Runs Ikiform's validator in your browser. Nothing is sent.

How useful was this?

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.

<form action="https://ikiform.com/f/frm_xxxxxxxxxxxxxxxx" method="post">
  <label for="rating">How useful was this?</label>
  <input id="rating" name="rating" type="number" min="1" max="5" step="1" required />
  <label for="comment">What would make it better?</label>
  <textarea id="comment" name="comment" rows="4" maxlength="4000"></textarea>
  <label for="email">Email, if you'd like a reply</label>
  <input id="email" name="email" type="email" autocomplete="email" />
  <input name="_gotcha" type="text" tabindex="-1" autocomplete="off" hidden />
  <button>Send feedback</button>
</form>
  • 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 and Spam protection.

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 idTypeRequiredAccepts
ratingratingYes1 to 5
commenttextareaNoUp to 4,000 characters
emailemailNoOne valid address
Field definition
{
  "version": 1,
  "title": "Feedback",
  "fields": [
    {
      "id": "rating",
      "type": "rating",
      "label": "How useful was this?",
      "required": true,
      "validation": {
        "max": 5
      }
    },
    {
      "id": "comment",
      "type": "textarea",
      "label": "What would make it better?",
      "rows": 4,
      "validation": {
        "max_length": 4000
      }
    },
    {
      "id": "email",
      "type": "email",
      "label": "Email, if you'd like a reply"
    }
  ],
  "settings": {
    "submit_label": "Send feedback",
    "success_message": "Thanks for the feedback."
  }
}

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
IKIFORM_SECRET_KEY=ik_sk_your_secret_key
app/feedback/actions.ts
"use server";

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

export async function sendFeedbackForm(_previous: FeedbackFormState, formData: FormData): Promise<FeedbackFormState> {
  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 the feedback." };
  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/feedback/form.tsx
"use client";

import { useActionState } from "react";
import { type FeedbackFormState, sendFeedbackForm } from "./actions";

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

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

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

  return (
    <form action={action}>
      <label htmlFor="rating">How useful was this?</label>
      <input id="rating" name="rating" type="number" min={1} max={5} step={1} required defaultValue={state.values?.rating?.[0]} />
      {state.fields?.rating ? <p>{state.fields.rating[0]}</p> : null}
      <label htmlFor="comment">What would make it better?</label>
      <textarea id="comment" name="comment" rows={4} maxLength={4000} defaultValue={state.values?.comment?.[0]} />
      {state.fields?.comment ? <p>{state.fields.comment[0]}</p> : null}
      <label htmlFor="email">Email, if you'd like a reply</label>
      <input id="email" name="email" type="email" autoComplete="email" defaultValue={state.values?.email?.[0]} />
      {state.fields?.email ? <p>{state.fields.email[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" : "Send feedback"}</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 for forms you send through a server.

Set it up

  1. Create a form in the 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. Feedback is one post per visit. Standard leaves room for a second thought. 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.

The rating field

A rating accepts a whole number from 1 to max, which is 5 here and can be 2 to 10. It is stored as a number, so averages and charts need no parsing. 0, 6 and 3.5 are refused.

Knowing which page it came from

Every submission records the visitor's user agent, origin, referrer and country alongside the fields. Webhooks include them under data.meta, so you don't need hidden fields for the page or the browser.

MetadataExample
referrerhttps://example.com/docs/install
originhttps://example.com
user_agentMozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)
countryGB

As a small widget

A feedback form often lives in a corner of every page. Post it with fetch or the SDK so the visitor stays on the page, and add each domain it appears on under Allowed domains. Keep the email optional: people give more honest feedback when they don't have to identify themselves.

Send low ratings somewhere the team will see them today, such as a Slack or Discord channel, and all ratings to a sheet or database for trends.

Where to send it

Every submission can go to a signed webhook. These guides cover the tools that suit this form, with the relay code:

Questions

Point your form at Ikiform