# Feedback form template

A free feedback form with a 5-star rating, a comment box and an optional email. Copy the HTML, CSS and JS, or use it in React, Next.js, Vue, Svelte or Astro.

This form starts with a five-star rating, asks one open question and makes the email field optional, so people can stay anonymous or ask for a reply. Only the rating is required.

Live preview, field editor, four themes and styled versions for every framework: https://nisuform.com/templates/feedback-form

## Fields

| Field | Name | Type | Required |
| --- | --- | --- | --- |
| Your rating | `rating` | Star rating | Yes |
| What could we do better? | `comment` | Long text | No |
| Email | `email` | Email | No |

## How to use it

1. Create a free form at https://app.nisuform.com and copy its form key.
2. Paste the code where the form should appear and replace `YOUR_FORM_KEY` with your key.
3. Submit the form once. The submission appears in your Nisuform inbox and you get an email.

## Styled HTML, CSS and JavaScript

The form with its styles and a small script that sends it without a page reload and shows the success message.

```html
<form id="nf-feedback-form" class="nf-form" action="https://api.nisuform.com/s/YOUR_FORM_KEY" method="POST">
  <style>
#nf-feedback-form {
  --nf-accent: #e60058;
  --nf-on-accent: #ffffff;
  --nf-text: #171717;
  --nf-muted: #525252;
  --nf-field: #f5f5f5;
  --nf-field-text: #171717;
  --nf-placeholder: #6b6b6b;
  --nf-control: #8a8a8a;
  --nf-error: #b91c1c;
  color-scheme: light;
  box-sizing: border-box;
  width: 100%;
  max-width: 480px;
  margin: 0;
  padding: 28px;
  border: 1px solid rgba(0, 0, 0, 0.08);
  border-radius: 16px;
  background: linear-gradient(180deg, #ffffff, #ffffff);
  box-shadow: 0 16px 40px -16px rgba(0, 0, 0, 0.12);
  color: var(--nf-text);
  font: inherit;
  font-size: 16px;
  line-height: 1.5;
  text-align: left;
}
#nf-feedback-form *, #nf-feedback-form *::before, #nf-feedback-form *::after { box-sizing: border-box; }
#nf-feedback-form [hidden] { display: none !important; }
#nf-feedback-form .nf-title { margin: 0; font-size: 1.25em; font-weight: 650; line-height: 1.25; letter-spacing: -0.01em; color: var(--nf-text); }
#nf-feedback-form .nf-desc { margin: 6px 0 0; font-size: 0.9375em; color: var(--nf-muted); }
#nf-feedback-form .nf-fields { display: grid; gap: 18px; margin-top: 22px; }
#nf-feedback-form .nf-head + .nf-fields { margin-top: 22px; }
#nf-feedback-form > .nf-fields:first-child { margin-top: 0; }
#nf-feedback-form .nf-field { margin: 0; padding: 0; border: 0; min-width: 0; }
#nf-feedback-form .nf-label { display: block; margin: 0 0 6px; padding: 0; font-size: 0.875em; font-weight: 600; color: var(--nf-text); }
#nf-feedback-form .nf-req { color: var(--nf-muted); font-weight: 400; }
#nf-feedback-form .nf-help { margin: 6px 0 0; font-size: 0.8125em; color: var(--nf-muted); }
#nf-feedback-form .nf-input { display: block; width: 100%; margin: 0; padding: 10px 12px; border: 1px solid transparent; border-radius: 10px; background: var(--nf-field); color: var(--nf-field-text); font: inherit; font-size: 1em; line-height: 1.4; outline: 2px solid transparent; outline-offset: 1px; transition: border-color 150ms, box-shadow 150ms; }
#nf-feedback-form input.nf-input, #nf-feedback-form select.nf-input { height: 2.75em; padding-top: 0; padding-bottom: 0; }
#nf-feedback-form textarea.nf-input { min-height: 112px; padding-top: 10px; padding-bottom: 10px; resize: vertical; }
#nf-feedback-form .nf-input::placeholder { color: var(--nf-placeholder); opacity: 1; }
#nf-feedback-form .nf-input:hover { border-color: color-mix(in srgb, var(--nf-text) 14%, transparent); }
#nf-feedback-form .nf-input:focus { border-color: var(--nf-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--nf-accent) 24%, transparent); }
#nf-feedback-form .nf-input:user-invalid { border-color: var(--nf-error); }
#nf-feedback-form .nf-input:user-invalid:focus { box-shadow: 0 0 0 3px color-mix(in srgb, var(--nf-error) 24%, transparent); }
#nf-feedback-form .nf-input:-webkit-autofill { -webkit-text-fill-color: var(--nf-field-text); caret-color: var(--nf-field-text); box-shadow: inset 0 0 0 100px var(--nf-field); }
#nf-feedback-form .nf-input:-webkit-autofill:focus { box-shadow: inset 0 0 0 100px var(--nf-field), 0 0 0 3px color-mix(in srgb, var(--nf-accent) 24%, transparent); }
#nf-feedback-form input[type="number"].nf-input { -moz-appearance: textfield; appearance: textfield; }
#nf-feedback-form input[type="number"].nf-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
#nf-feedback-form input[type="number"].nf-input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
#nf-feedback-form input[type="date"].nf-input { min-width: 0; text-align: left; }
#nf-feedback-form input[type="date"].nf-input::-webkit-date-and-time-value { text-align: left; }
#nf-feedback-form input[type="date"].nf-input.nf-empty:not(:focus) { color: var(--nf-placeholder); }
#nf-feedback-form input[type="date"].nf-input::-webkit-calendar-picker-indicator { margin: 0; padding: 4px; border-radius: 6px; opacity: 0.55; cursor: pointer; transition: opacity 150ms; }
#nf-feedback-form input[type="date"].nf-input::-webkit-calendar-picker-indicator:hover { opacity: 1; }
#nf-feedback-form select.nf-input { -webkit-appearance: none; appearance: none; padding-right: 40px; background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23525252%22%20stroke-width%3D%222%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M6%209l6%206l6%20-6%22%2F%3E%3C%2Fsvg%3E"); background-repeat: no-repeat; background-position: right 12px center; background-size: 16px 16px; cursor: pointer; }
#nf-feedback-form select.nf-input:has(option[value=""]:checked) { color: var(--nf-placeholder); }
#nf-feedback-form select.nf-input option { color: var(--nf-field-text); }
#nf-feedback-form .nf-options { display: grid; gap: 8px; }
#nf-feedback-form .nf-option { display: flex; align-items: center; gap: 10px; min-height: 2.75em; margin: 0; padding: 10px 12px; border: 1px solid color-mix(in srgb, var(--nf-text) 14%, transparent); border-radius: 10px; background: transparent; color: var(--nf-text); font-size: 0.9375em; line-height: 1.4; cursor: pointer; transition: border-color 150ms, background-color 150ms; }
#nf-feedback-form .nf-option:hover { border-color: color-mix(in srgb, var(--nf-text) 28%, transparent); }
#nf-feedback-form .nf-option:has(input:checked) { border-color: var(--nf-accent); background: color-mix(in srgb, var(--nf-accent) 8%, transparent); }
#nf-feedback-form .nf-option span { min-width: 0; overflow-wrap: anywhere; }
#nf-feedback-form .nf-option input { -webkit-appearance: none; appearance: none; flex: none; width: 18px; height: 18px; margin: 0; border: 1.5px solid var(--nf-control); border-radius: 50%; background: transparent; outline: 2px solid transparent; cursor: pointer; transition: border-color 150ms, box-shadow 150ms; }
#nf-feedback-form .nf-option input:checked { border-color: var(--nf-accent); background: radial-gradient(circle, var(--nf-on-accent) 0 3px, var(--nf-accent) 3.5px); }
#nf-feedback-form .nf-option input:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--nf-accent) 24%, transparent); }
#nf-feedback-form .nf-check { display: flex; align-items: flex-start; gap: 10px; margin: 0; font-size: 0.9375em; line-height: 1.5; color: var(--nf-text); cursor: pointer; }
#nf-feedback-form .nf-check input { -webkit-appearance: none; appearance: none; flex: none; width: 20px; height: 20px; margin: 1px 0 0; border: 1.5px solid var(--nf-control); border-radius: 6px; background: var(--nf-field) center / 14px 14px no-repeat; outline: 2px solid transparent; cursor: pointer; transition: background-color 150ms, border-color 150ms, box-shadow 150ms; }
#nf-feedback-form .nf-check input:checked { border-color: var(--nf-accent); background-color: var(--nf-accent); background-image: url("data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2024%2024%22%20fill%3D%22none%22%20stroke%3D%22%23ffffff%22%20stroke-width%3D%223.5%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%3E%3Cpath%20d%3D%22M5%2012l5%205l10%20-10%22%2F%3E%3C%2Fsvg%3E"); }
#nf-feedback-form .nf-check input:focus-visible { box-shadow: 0 0 0 3px color-mix(in srgb, var(--nf-accent) 24%, transparent); }
#nf-feedback-form .nf-check input:user-invalid { border-color: var(--nf-error); }
#nf-feedback-form .nf-rating { display: flex; flex-wrap: wrap; margin: 0 0 0 -4px; }
#nf-feedback-form .nf-rating label { display: inline-flex; padding: 4px; border-radius: 8px; color: var(--nf-control); color: color-mix(in srgb, var(--nf-control) 45%, transparent); cursor: pointer; transition: color 150ms; }
#nf-feedback-form .nf-rating svg { width: 30px; height: 30px; fill: currentColor; stroke: currentColor; stroke-width: 1.5; stroke-linejoin: round; transition: transform 150ms; }
#nf-feedback-form .nf-rating input:checked + label, #nf-feedback-form .nf-rating label:has(~ input:checked) { color: var(--nf-accent); }
#nf-feedback-form .nf-rating:hover input + label { color: var(--nf-control); color: color-mix(in srgb, var(--nf-control) 45%, transparent); }
#nf-feedback-form .nf-rating:hover label:hover, #nf-feedback-form .nf-rating:hover label:has(~ label:hover) { color: var(--nf-accent); }
#nf-feedback-form .nf-rating label:active svg { transform: scale(0.88); }
#nf-feedback-form .nf-rating input:focus-visible + label { outline: 2px solid var(--nf-accent); outline-offset: 0; }
#nf-feedback-form .nf-file { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px; min-height: 112px; padding: 16px; border: 1.5px dashed var(--nf-control); border-color: color-mix(in srgb, var(--nf-control) 55%, transparent); border-radius: 10px; background: var(--nf-field); color: var(--nf-muted); text-align: center; transition: border-color 150ms, background-color 150ms, box-shadow 150ms; }
#nf-feedback-form .nf-file-input { position: absolute; top: 0; left: 0; width: 100%; height: 100%; margin: 0; opacity: 0; cursor: pointer; }
#nf-feedback-form .nf-file svg { width: 22px; height: 22px; margin-bottom: 4px; }
#nf-feedback-form .nf-file-text { margin: 0; max-width: 100%; font-size: 0.875em; overflow-wrap: anywhere; }
#nf-feedback-form .nf-file-cta { font-weight: 600; color: var(--nf-text); }
#nf-feedback-form .nf-file-types { margin: 0; font-size: 0.75em; color: var(--nf-placeholder); }
#nf-feedback-form .nf-file:hover { border-color: var(--nf-control); }
#nf-feedback-form .nf-file:focus-within { border-color: var(--nf-accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--nf-accent) 24%, transparent); }
#nf-feedback-form .nf-file.nf-file-over { border-color: var(--nf-accent); background: color-mix(in srgb, var(--nf-accent) 8%, var(--nf-field)); }
#nf-feedback-form .nf-file.nf-file-chosen { border-style: solid; border-color: var(--nf-accent); }
#nf-feedback-form .nf-file.nf-file-chosen .nf-file-text { font-weight: 600; color: var(--nf-text); }
#nf-feedback-form .nf-file:has(.nf-file-input:user-invalid) { border-color: var(--nf-error); }
#nf-feedback-form .nf-sr { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
#nf-feedback-form .nf-hp { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; }
#nf-feedback-form .nf-captcha { margin-top: 18px; }
#nf-feedback-form .nf-submit { display: inline-flex; align-items: center; justify-content: center; width: 100%; min-height: 44px; margin: 22px 0 0; padding: 10px 18px; border: 0; border-radius: 10px; background: var(--nf-accent); color: var(--nf-on-accent); font: inherit; font-size: 1em; font-weight: 600; cursor: pointer; transition: filter 150ms, transform 150ms; }
#nf-feedback-form .nf-submit:hover { filter: brightness(0.94); }
#nf-feedback-form .nf-submit:active { transform: scale(0.98); }
#nf-feedback-form .nf-submit:focus-visible { outline: 2px solid var(--nf-accent); outline-offset: 3px; }
#nf-feedback-form .nf-submit:disabled { opacity: 0.6; cursor: default; }
#nf-feedback-form .nf-error { margin: 14px 0 0; padding: 10px 12px; border-radius: 10px; background: color-mix(in srgb, var(--nf-error) 10%, transparent); font-size: 0.875em; color: var(--nf-error); }
#nf-feedback-form.nf-sent { display: flex; flex-direction: column; justify-content: center; min-height: min(var(--nf-height, 0px), 440px); }
#nf-feedback-form .nf-success { display: flex; flex-direction: column; align-items: center; padding: 20px 0; text-align: center; outline: none; }
#nf-feedback-form .nf-success-icon { display: flex; flex: none; align-items: center; justify-content: center; width: 56px; height: 56px; margin: 0 0 20px; border-radius: 50%; background: var(--nf-accent); color: var(--nf-on-accent); box-shadow: 0 0 0 8px color-mix(in srgb, var(--nf-accent) 12%, transparent); }
#nf-feedback-form .nf-success-icon svg { display: block; width: 28px; height: 28px; margin: 0; stroke-dasharray: 1; }
#nf-feedback-form .nf-success-title { margin: 0; max-width: 24em; font-size: 1.125em; font-weight: 650; line-height: 1.35; letter-spacing: -0.01em; color: var(--nf-text); text-wrap: balance; overflow-wrap: anywhere; }
#nf-feedback-form .nf-success-text { margin: 0; max-width: 30em; font-size: 0.9375em; line-height: 1.5; color: var(--nf-text); text-wrap: pretty; overflow-wrap: anywhere; }
#nf-feedback-form .nf-success-title + .nf-success-text { margin-top: 6px; color: var(--nf-muted); }
#nf-feedback-form.nf-sent .nf-success { animation: nf-success-in 400ms cubic-bezier(0.16, 1, 0.3, 1) both; }
#nf-feedback-form.nf-sent .nf-success-icon { animation: nf-success-pop 500ms cubic-bezier(0.34, 1.56, 0.64, 1) both; }
#nf-feedback-form.nf-sent .nf-success-icon path { animation: nf-success-draw 360ms 160ms cubic-bezier(0.65, 0, 0.35, 1) both; }
@keyframes nf-success-in { from { opacity: 0; transform: translateY(6px); } }
@keyframes nf-success-pop { from { transform: scale(0.6); } }
@keyframes nf-success-draw { from { stroke-dashoffset: 1; } }
@media (prefers-reduced-motion: reduce) { #nf-feedback-form * { transition: none !important; animation: none !important; } }
  </style>
  <div class="nf-head">
    <h2 class="nf-title">How did we do?</h2>
    <p class="nf-desc">It takes ten seconds and helps us a lot.</p>
  </div>
  <div class="nf-fields">
    <fieldset class="nf-field nf-group">
      <legend class="nf-label">Your rating<span class="nf-req" aria-hidden="true"> *</span></legend>
      <div class="nf-rating">
        <input class="nf-sr" type="radio" id="nf-feedback-form-rating-1" name="rating" value="1" required>
        <label for="nf-feedback-form-rating-1"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z"/></svg><span class="nf-sr">1 star</span></label>
        <input class="nf-sr" type="radio" id="nf-feedback-form-rating-2" name="rating" value="2">
        <label for="nf-feedback-form-rating-2"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z"/></svg><span class="nf-sr">2 stars</span></label>
        <input class="nf-sr" type="radio" id="nf-feedback-form-rating-3" name="rating" value="3">
        <label for="nf-feedback-form-rating-3"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z"/></svg><span class="nf-sr">3 stars</span></label>
        <input class="nf-sr" type="radio" id="nf-feedback-form-rating-4" name="rating" value="4">
        <label for="nf-feedback-form-rating-4"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z"/></svg><span class="nf-sr">4 stars</span></label>
        <input class="nf-sr" type="radio" id="nf-feedback-form-rating-5" name="rating" value="5">
        <label for="nf-feedback-form-rating-5"><svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z"/></svg><span class="nf-sr">5 stars</span></label>
      </div>
    </fieldset>
    <div class="nf-field">
      <label class="nf-label" for="nf-feedback-form-comment">What could we do better?</label>
      <textarea class="nf-input" id="nf-feedback-form-comment" name="comment" rows="4"></textarea>
    </div>
    <div class="nf-field">
      <label class="nf-label" for="nf-feedback-form-email">Email</label>
      <input class="nf-input" id="nf-feedback-form-email" name="email" type="email" autocomplete="email" placeholder="you@example.com" aria-describedby="nf-feedback-form-email-help">
      <p class="nf-help" id="nf-feedback-form-email-help">Optional. Leave it if you would like a reply.</p>
    </div>
  </div>
  <div class="nf-hp" aria-hidden="true"><input type="text" name="_gotcha" tabindex="-1" autocomplete="off"></div>
  <button class="nf-submit" type="submit">Send feedback</button>
  <p class="nf-error" role="alert" hidden></p>
  <div class="nf-success" role="status" tabindex="-1" hidden>
    <span class="nf-success-icon" aria-hidden="true"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" focusable="false"><path d="M5 12l5 5l10 -10" pathLength="1"/></svg></span>
    <p class="nf-success-title">Thanks for the feedback!</p>
  </div>
</form>
<script>
(function () {
  var form = document.getElementById("nf-feedback-form")
  if (!form || !window.fetch) return
  var started = Date.now()
  var button = form.querySelector('.nf-submit')
  var error = form.querySelector('.nf-error')
  var success = form.querySelector('.nf-success')
  form.addEventListener('submit', function (event) {
    event.preventDefault()
    var data = new FormData(form)
    data.set('_elapsed', String(Date.now() - started))
    var label = button.textContent
    button.disabled = true
    button.textContent = 'Sending...'
    error.hidden = true
    fetch(form.action, {
      method: 'POST',
      headers: { Accept: 'application/json' },
      body: new URLSearchParams(data),
    })
      .then(function (response) {
        return response.json().catch(function () { return {} }).then(function (body) {
          if (!response.ok || !body.ok) throw new Error(body.message || 'Something went wrong. Please try again.')
        })
      })
      .then(function () {
        var redirect = form.getAttribute('data-redirect')
        if (redirect) {
          window.location.href = redirect
          return
        }
        form.style.setProperty('--nf-height', form.offsetHeight + 'px')
        Array.prototype.forEach.call(form.children, function (child) {
          if (child !== success && child.tagName !== 'STYLE') child.hidden = true
        })
        form.classList.add('nf-sent')
        success.hidden = false
        success.focus()
      })
      .catch(function (reason) {
        error.textContent = reason && reason.message ? reason.message : 'Something went wrong. Please try again.'
        error.hidden = false
      })
      .finally(function () {
        button.disabled = false
        button.textContent = label
        if (window.turnstile) window.turnstile.reset()
      })
  })
})()
</script>
```

## Unstyled HTML

Bare markup that picks up your own CSS. It posts without JavaScript and keeps the hidden `_gotcha` field that stops bots.

```html
<form action="https://api.nisuform.com/s/YOUR_FORM_KEY" method="POST">
  <fieldset>
    <legend>Your rating</legend>
    <label><input type="radio" name="rating" value="1" required> 1</label>
    <label><input type="radio" name="rating" value="2"> 2</label>
    <label><input type="radio" name="rating" value="3"> 3</label>
    <label><input type="radio" name="rating" value="4"> 4</label>
    <label><input type="radio" name="rating" value="5"> 5</label>
  </fieldset>
  <label>
    What could we do better?
    <textarea name="comment" rows="5"></textarea>
  </label>
  <label>
    Email
    <input name="email" type="email" placeholder="you@example.com">
  </label>
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none">
  <button type="submit">Send feedback</button>
</form>
```

## Framework components

### React

```jsx
import { useRef, useState } from 'react'

export function FeedbackForm() {
  const renderedAt = useRef(Date.now())
  const [status, setStatus] = useState('idle')

  async function onSubmit(event) {
    event.preventDefault()
    setStatus('sending')
    const data = Object.fromEntries(new FormData(event.currentTarget))
    data._elapsed = String(Date.now() - renderedAt.current)
    const response = await fetch("https://api.nisuform.com/s/YOUR_FORM_KEY", {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
      body: JSON.stringify(data),
    })
    setStatus(response.ok ? 'sent' : 'error')
  }

  if (status === 'sent') return <p>Thanks for the feedback!</p>

  return (
    <form onSubmit={onSubmit}>
      <fieldset>
        <legend>Your rating</legend>
        <label><input type="radio" name="rating" value="1" required /> 1</label>
        <label><input type="radio" name="rating" value="2" /> 2</label>
        <label><input type="radio" name="rating" value="3" /> 3</label>
        <label><input type="radio" name="rating" value="4" /> 4</label>
        <label><input type="radio" name="rating" value="5" /> 5</label>
      </fieldset>
      <label>
        What could we do better?
        <textarea name="comment" rows={5}></textarea>
      </label>
      <label>
        Email
        <input name="email" type="email" placeholder="you@example.com" />
      </label>
      <input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: 'none' }} />
      <button type="submit" disabled={status === 'sending'}>Send feedback</button>
      {status === 'error' && <p role="alert">Something went wrong. Please try again.</p>}
    </form>
  )
}
```

### Next.js

```jsx
'use client'

import { useRef, useState } from 'react'

export function FeedbackForm() {
  const renderedAt = useRef(Date.now())
  const [status, setStatus] = useState('idle')

  async function onSubmit(event) {
    event.preventDefault()
    setStatus('sending')
    const data = Object.fromEntries(new FormData(event.currentTarget))
    data._elapsed = String(Date.now() - renderedAt.current)
    const response = await fetch("https://api.nisuform.com/s/YOUR_FORM_KEY", {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
      body: JSON.stringify(data),
    })
    setStatus(response.ok ? 'sent' : 'error')
  }

  if (status === 'sent') return <p>Thanks for the feedback!</p>

  return (
    <form onSubmit={onSubmit}>
      <fieldset>
        <legend>Your rating</legend>
        <label><input type="radio" name="rating" value="1" required /> 1</label>
        <label><input type="radio" name="rating" value="2" /> 2</label>
        <label><input type="radio" name="rating" value="3" /> 3</label>
        <label><input type="radio" name="rating" value="4" /> 4</label>
        <label><input type="radio" name="rating" value="5" /> 5</label>
      </fieldset>
      <label>
        What could we do better?
        <textarea name="comment" rows={5}></textarea>
      </label>
      <label>
        Email
        <input name="email" type="email" placeholder="you@example.com" />
      </label>
      <input type="text" name="_gotcha" tabIndex={-1} autoComplete="off" style={{ display: 'none' }} />
      <button type="submit" disabled={status === 'sending'}>Send feedback</button>
      {status === 'error' && <p role="alert">Something went wrong. Please try again.</p>}
    </form>
  )
}
```

### Vue

```vue
<script setup>
import { ref } from 'vue'

const shownAt = Date.now()
const status = ref('idle')

async function onSubmit(event) {
  status.value = 'sending'
  const data = new FormData(event.target)
  data.set('_elapsed', String(Date.now() - shownAt))
  const response = await fetch("https://api.nisuform.com/s/YOUR_FORM_KEY", {
    method: 'POST',
    headers: { Accept: 'application/json' },
    body: data,
  })
  status.value = response.ok ? 'sent' : 'error'
}
</script>

<template>
  <p v-if="status === 'sent'">Thanks for the feedback!</p>
  <form v-else @submit.prevent="onSubmit">
    <fieldset>
      <legend>Your rating</legend>
      <label><input type="radio" name="rating" value="1" required> 1</label>
      <label><input type="radio" name="rating" value="2"> 2</label>
      <label><input type="radio" name="rating" value="3"> 3</label>
      <label><input type="radio" name="rating" value="4"> 4</label>
      <label><input type="radio" name="rating" value="5"> 5</label>
    </fieldset>
    <label>
      What could we do better?
      <textarea name="comment" rows="5"></textarea>
    </label>
    <label>
      Email
      <input name="email" type="email" placeholder="you@example.com">
    </label>
    <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none">
    <button type="submit" :disabled="status === 'sending'">Send feedback</button>
    <p v-if="status === 'error'" role="alert">Something went wrong. Please try again.</p>
  </form>
</template>
```

### Svelte

```svelte
<script>
  const shownAt = Date.now()
  let status = $state('idle')

  async function onsubmit(event) {
    event.preventDefault()
    status = 'sending'
    const data = new FormData(event.currentTarget)
    data.set('_elapsed', String(Date.now() - shownAt))
    const response = await fetch("https://api.nisuform.com/s/YOUR_FORM_KEY", {
      method: 'POST',
      headers: { Accept: 'application/json' },
      body: data,
    })
    status = response.ok ? 'sent' : 'error'
  }
</script>

{#if status === 'sent'}
  <p>Thanks for the feedback!</p>
{:else}
  <form {onsubmit}>
    <fieldset>
      <legend>Your rating</legend>
      <label><input type="radio" name="rating" value="1" required> 1</label>
      <label><input type="radio" name="rating" value="2"> 2</label>
      <label><input type="radio" name="rating" value="3"> 3</label>
      <label><input type="radio" name="rating" value="4"> 4</label>
      <label><input type="radio" name="rating" value="5"> 5</label>
    </fieldset>
    <label>
      What could we do better?
      <textarea name="comment" rows="5"></textarea>
    </label>
    <label>
      Email
      <input name="email" type="email" placeholder="you@example.com">
    </label>
    <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none">
    <button type="submit" disabled={status === 'sending'}>Send feedback</button>
    {#if status === 'error'}
      <p role="alert">Something went wrong. Please try again.</p>
    {/if}
  </form>
{/if}
```

### Astro

```astro
---
const endpoint = "https://api.nisuform.com/s/YOUR_FORM_KEY"
---

<form action={endpoint} method="POST">
  <fieldset>
    <legend>Your rating</legend>
    <label><input type="radio" name="rating" value="1" required> 1</label>
    <label><input type="radio" name="rating" value="2"> 2</label>
    <label><input type="radio" name="rating" value="3"> 3</label>
    <label><input type="radio" name="rating" value="4"> 4</label>
    <label><input type="radio" name="rating" value="5"> 5</label>
  </fieldset>
  <label>
    What could we do better?
    <textarea name="comment" rows="5"></textarea>
  </label>
  <label>
    Email
    <input name="email" type="email" placeholder="you@example.com">
  </label>
  <input type="text" name="_gotcha" tabindex="-1" autocomplete="off" style="display:none">
  <button type="submit">Send feedback</button>
</form>
```

## When to use it

- Product or website feedback after a purchase or signup
- Rating a workshop, class or event
- Customer satisfaction checks after a support conversation
- A "Was this page helpful?" form on documentation

## Customize it

### Change the scale

The stars are radio inputs named `rating` with values 1 to 5. Add or remove inputs for a different scale. They are styled with CSS only, so the rating works without JavaScript.

### See the average rating

Forms created from the Feedback template in the dashboard know which field is the rating, so Analytics on Pro shows the average and how many people picked each star.

### Keep it anonymous

Remove the email field and turn off IP address storage for the form. Submissions are then saved without anything that identifies the sender.

## Frequently asked questions

### How do I make a star rating in HTML?

Use a group of radio inputs that share a name, one per star, each with a label. This template styles the labels as stars with CSS and keeps the inputs for keyboards and screen readers, so the rating works without JavaScript.

### Can feedback be anonymous?

Yes. The email field is optional, and you can turn off IP address storage in the form settings. Submissions are then stored without personal data the visitor did not type in.

### Where do I read the feedback?

Every submission is saved in your Nisuform inbox and emailed to you. You can search it, mark it read or unread, star it and export it as CSV.
