Complaint form template
What happened, when, the reference to look it up, and what would put it right.
Reviewed by Ritu Fadadu · Last reviewed
Preview and code
Preview
Code — replace YOUR_ENDPOINT_ID with your form's ID
<style>
.fb-form { display: grid; gap: 1rem; max-width: 36rem; }
.fb-field { display: flex; flex-direction: column; gap: 0.375rem; }
.fb-fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }
.fb-label { font-weight: 600; }
.fb-optional { font-weight: 400; color: #6b7280; }
.fb-help { margin: 0; font-size: 0.875rem; color: #6b7280; }
.fb-input { box-sizing: border-box; width: 100%; padding: 0.625rem 0.75rem; border: 1px solid #d1d5db; border-radius: 0.5rem; background: #fff; color: #111827; font: inherit; }
.fb-input:focus { border-color: #2563eb; outline: 2px solid #2563eb; outline-offset: 1px; }
.fb-options { display: flex; flex-direction: column; gap: 0.5rem; }
.fb-option { display: flex; align-items: center; gap: 0.5rem; }
.fb-check { width: 1.125rem; height: 1.125rem; margin: 0; accent-color: #2563eb; }
.fb-button { justify-self: start; min-height: 44px; padding: 0.625rem 1.25rem; border: 0; border-radius: 0.5rem; background: #2563eb; color: #fff; font: inherit; font-weight: 600; cursor: pointer; }
.fb-button:hover { background: #1d4ed8; }
.fb-button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.fb-button:disabled { opacity: 0.6; cursor: wait; }
.fb-status { margin: 0; min-height: 1.5em; }
@media (min-width: 640px) {
.fb-form { grid-template-columns: 1fr 1fr; }
.fb-field, .fb-button, .fb-status { grid-column: 1 / -1; }
.fb-field--half { grid-column: auto; }
}
</style>
<form id="fb-form" class="fb-form" action="https://formboost.app/f/YOUR_ENDPOINT_ID" method="POST">
<div class="fb-field fb-field--half">
<label for="fb-name" class="fb-label">Name</label>
<input type="text" id="fb-name" name="name" class="fb-input" required autocomplete="name">
</div>
<div class="fb-field fb-field--half">
<label for="fb-email" class="fb-label">Email</label>
<input type="email" id="fb-email" name="email" class="fb-input" required autocomplete="email">
</div>
<div class="fb-field fb-field--half">
<label for="fb-category" class="fb-label">What is it about?</label>
<select id="fb-category" name="category" class="fb-input" required>
<option value="">Choose…</option>
<option value="a-product">A product</option>
<option value="a-service">A service</option>
<option value="billing">Billing</option>
<option value="a-member-of-staff">A member of staff</option>
<option value="something-else">Something else</option>
</select>
</div>
<div class="fb-field fb-field--half">
<label for="fb-reference" class="fb-label">
Order or reference number
<span class="fb-optional">(optional)</span>
</label>
<input type="text" id="fb-reference" name="reference" class="fb-input">
</div>
<div class="fb-field fb-field--half">
<label for="fb-incident_date" class="fb-label">
When did it happen?
<span class="fb-optional">(optional)</span>
</label>
<input type="date" id="fb-incident_date" name="incident_date" class="fb-input">
</div>
<div class="fb-field">
<label for="fb-details" class="fb-label">Tell us what happened</label>
<textarea id="fb-details" name="details" class="fb-input" required rows="6"></textarea>
</div>
<div class="fb-field">
<label for="fb-resolution" class="fb-label">
What would put this right?
<span class="fb-optional">(optional)</span>
</label>
<textarea id="fb-resolution" name="resolution" class="fb-input" rows="3"></textarea>
</div>
<fieldset class="fb-field fb-fieldset">
<legend class="fb-label">
How should we contact you?
<span class="fb-optional">(optional)</span>
</legend>
<div class="fb-options">
<label class="fb-option">
<input type="radio" name="contact_method" value="email" class="fb-check">
<span>Email</span>
</label>
<label class="fb-option">
<input type="radio" name="contact_method" value="phone" class="fb-check">
<span>Phone</span>
</label>
</div>
</fieldset>
<!-- Honeypot: keep it hidden. People never see it; bots fill it in and get flagged as spam. -->
<div
aria-hidden="true"
style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden"
>
<label for="fb-honey">Leave this field empty</label>
<input type="text" id="fb-honey" name="_honey" tabindex="-1" autocomplete="off">
</div>
<input type="hidden" name="_subject" value="New complaint">
<button type="submit" class="fb-button">Send complaint</button>
</form>
<style>
.fb-form { display: grid; gap: 1rem; max-width: 36rem; }
.fb-field { display: flex; flex-direction: column; gap: 0.375rem; }
.fb-fieldset { margin: 0; padding: 0; border: 0; min-width: 0; }
.fb-label { font-weight: 600; }
.fb-optional { font-weight: 400; color: #6b7280; }
.fb-help { margin: 0; font-size: 0.875rem; color: #6b7280; }
.fb-input { box-sizing: border-box; width: 100%; padding: 0.625rem 0.75rem; border: 1px solid #d1d5db; border-radius: 0.5rem; background: #fff; color: #111827; font: inherit; }
.fb-input:focus { border-color: #2563eb; outline: 2px solid #2563eb; outline-offset: 1px; }
.fb-options { display: flex; flex-direction: column; gap: 0.5rem; }
.fb-option { display: flex; align-items: center; gap: 0.5rem; }
.fb-check { width: 1.125rem; height: 1.125rem; margin: 0; accent-color: #2563eb; }
.fb-button { justify-self: start; min-height: 44px; padding: 0.625rem 1.25rem; border: 0; border-radius: 0.5rem; background: #2563eb; color: #fff; font: inherit; font-weight: 600; cursor: pointer; }
.fb-button:hover { background: #1d4ed8; }
.fb-button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
.fb-button:disabled { opacity: 0.6; cursor: wait; }
.fb-status { margin: 0; min-height: 1.5em; }
@media (min-width: 640px) {
.fb-form { grid-template-columns: 1fr 1fr; }
.fb-field, .fb-button, .fb-status { grid-column: 1 / -1; }
.fb-field--half { grid-column: auto; }
}
</style>
<form id="fb-form" class="fb-form" action="https://formboost.app/f/YOUR_ENDPOINT_ID" method="POST">
<div class="fb-field fb-field--half">
<label for="fb-name" class="fb-label">Name</label>
<input type="text" id="fb-name" name="name" class="fb-input" required autocomplete="name">
</div>
<div class="fb-field fb-field--half">
<label for="fb-email" class="fb-label">Email</label>
<input type="email" id="fb-email" name="email" class="fb-input" required autocomplete="email">
</div>
<div class="fb-field fb-field--half">
<label for="fb-category" class="fb-label">What is it about?</label>
<select id="fb-category" name="category" class="fb-input" required>
<option value="">Choose…</option>
<option value="a-product">A product</option>
<option value="a-service">A service</option>
<option value="billing">Billing</option>
<option value="a-member-of-staff">A member of staff</option>
<option value="something-else">Something else</option>
</select>
</div>
<div class="fb-field fb-field--half">
<label for="fb-reference" class="fb-label">
Order or reference number
<span class="fb-optional">(optional)</span>
</label>
<input type="text" id="fb-reference" name="reference" class="fb-input">
</div>
<div class="fb-field fb-field--half">
<label for="fb-incident_date" class="fb-label">
When did it happen?
<span class="fb-optional">(optional)</span>
</label>
<input type="date" id="fb-incident_date" name="incident_date" class="fb-input">
</div>
<div class="fb-field">
<label for="fb-details" class="fb-label">Tell us what happened</label>
<textarea id="fb-details" name="details" class="fb-input" required rows="6"></textarea>
</div>
<div class="fb-field">
<label for="fb-resolution" class="fb-label">
What would put this right?
<span class="fb-optional">(optional)</span>
</label>
<textarea id="fb-resolution" name="resolution" class="fb-input" rows="3"></textarea>
</div>
<fieldset class="fb-field fb-fieldset">
<legend class="fb-label">
How should we contact you?
<span class="fb-optional">(optional)</span>
</legend>
<div class="fb-options">
<label class="fb-option">
<input type="radio" name="contact_method" value="email" class="fb-check">
<span>Email</span>
</label>
<label class="fb-option">
<input type="radio" name="contact_method" value="phone" class="fb-check">
<span>Phone</span>
</label>
</div>
</fieldset>
<!-- Honeypot: keep it hidden. People never see it; bots fill it in and get flagged as spam. -->
<div
aria-hidden="true"
style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden"
>
<label for="fb-honey">Leave this field empty</label>
<input type="text" id="fb-honey" name="_honey" tabindex="-1" autocomplete="off">
</div>
<input type="hidden" name="_subject" value="New complaint">
<button type="submit" class="fb-button">Send complaint</button>
<p class="fb-status" role="status" aria-live="polite" data-fb-status></p>
</form>
<script>
document.getElementById("fb-form").addEventListener("submit", async (event) => {
event.preventDefault();
const form = event.currentTarget;
const status = form.querySelector("[data-fb-status]");
const button = form.querySelector('button[type="submit"]');
const fields = new FormData(form);
// Formboost accepts JSON or url-encoded bodies, not multipart, so send JSON.
const data = Object.fromEntries(fields);
// Makes the notification email replyable; the address still shows in the field table.
if (data.email) data._replyto = data.email;
button.disabled = true;
status.textContent = "Sending…";
try {
const response = await fetch(form.action, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
form.reset();
status.textContent = "Thank you. We've received your complaint and will respond soon.";
} else if (response.status === 429) {
status.textContent = "Too many attempts. Please wait a minute and try again.";
} else {
status.textContent = "Something went wrong. Please try again.";
}
} catch {
status.textContent = "Could not send. Check your connection and try again.";
} finally {
button.disabled = false;
}
});
</script>
import { useState } from "react";
// Your form's endpoint, from the Formboost dashboard.
const ENDPOINT = "https://formboost.app/f/YOUR_ENDPOINT_ID";
export default function ComplaintForm() {
const [sending, setSending] = useState(false);
const [message, setMessage] = useState("");
async function handleSubmit(event) {
event.preventDefault();
const form = event.currentTarget;
const fields = new FormData(form);
// Formboost accepts JSON or url-encoded bodies, not multipart, so send JSON.
const data = Object.fromEntries(fields);
// Makes the notification email replyable; the address still shows in the field table.
if (data.email) data._replyto = data.email;
setSending(true);
setMessage("Sending…");
try {
const response = await fetch(ENDPOINT, {
method: "POST",
headers: { "Content-Type": "application/json", Accept: "application/json" },
body: JSON.stringify(data),
});
if (response.ok) {
form.reset();
setMessage("Thank you. We've received your complaint and will respond soon.");
} else if (response.status === 429) {
setMessage("Too many attempts. Please wait a minute and try again.");
} else {
setMessage("Something went wrong. Please try again.");
}
} catch {
setMessage("Could not send. Check your connection and try again.");
} finally {
setSending(false);
}
}
return (
<form
className="grid max-w-xl gap-4 sm:grid-cols-2"
action={ENDPOINT}
method="POST"
onSubmit={handleSubmit}
>
<div className="flex flex-col gap-1.5">
<label htmlFor="fb-name" className="text-sm font-semibold text-gray-900">Name</label>
<input
type="text"
id="fb-name"
name="name"
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
required
autoComplete="name"
/>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="fb-email" className="text-sm font-semibold text-gray-900">Email</label>
<input
type="email"
id="fb-email"
name="email"
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
required
autoComplete="email"
/>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="fb-category" className="text-sm font-semibold text-gray-900">
What is it about?
</label>
<select
id="fb-category"
name="category"
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
required
>
<option value="">Choose…</option>
<option value="a-product">A product</option>
<option value="a-service">A service</option>
<option value="billing">Billing</option>
<option value="a-member-of-staff">A member of staff</option>
<option value="something-else">Something else</option>
</select>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="fb-reference" className="text-sm font-semibold text-gray-900">
Order or reference number
<span className="font-normal text-gray-500">(optional)</span>
</label>
<input
type="text"
id="fb-reference"
name="reference"
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
/>
</div>
<div className="flex flex-col gap-1.5">
<label htmlFor="fb-incident_date" className="text-sm font-semibold text-gray-900">
When did it happen?
<span className="font-normal text-gray-500">(optional)</span>
</label>
<input
type="date"
id="fb-incident_date"
name="incident_date"
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
/>
</div>
<div className="flex flex-col gap-1.5 sm:col-span-2">
<label htmlFor="fb-details" className="text-sm font-semibold text-gray-900">
Tell us what happened
</label>
<textarea
id="fb-details"
name="details"
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
required
rows={6}
></textarea>
</div>
<div className="flex flex-col gap-1.5 sm:col-span-2">
<label htmlFor="fb-resolution" className="text-sm font-semibold text-gray-900">
What would put this right?
<span className="font-normal text-gray-500">(optional)</span>
</label>
<textarea
id="fb-resolution"
name="resolution"
className="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
rows={3}
></textarea>
</div>
<fieldset className="flex flex-col gap-1.5 sm:col-span-2 m-0 min-w-0 border-0 p-0">
<legend className="text-sm font-semibold text-gray-900">
How should we contact you?
<span className="font-normal text-gray-500">(optional)</span>
</legend>
<div className="flex flex-col gap-2">
<label className="flex items-center gap-2 text-gray-800">
<input
type="radio"
name="contact_method"
value="email"
className="h-4 w-4 accent-blue-600"
/>
<span>Email</span>
</label>
<label className="flex items-center gap-2 text-gray-800">
<input
type="radio"
name="contact_method"
value="phone"
className="h-4 w-4 accent-blue-600"
/>
<span>Phone</span>
</label>
</div>
</fieldset>
{/* Honeypot: keep it hidden. People never see it; bots fill it in and get flagged as spam. */}
<div
aria-hidden="true"
style={{ position: "absolute", left: "-9999px", width: "1px", height: "1px", overflow: "hidden" }}
>
<label htmlFor="fb-honey">Leave this field empty</label>
<input type="text" id="fb-honey" name="_honey" tabIndex={-1} autoComplete="off" />
</div>
<input type="hidden" name="_subject" defaultValue="New complaint" />
<button
type="submit"
className="min-h-11 justify-self-start rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-60 sm:col-span-2"
disabled={sending}
>
Send complaint
</button>
<p className="min-h-6 text-sm text-gray-700 sm:col-span-2" role="status" aria-live="polite">
{message}
</p>
</form>
);
}
<form
id="fb-form"
class="grid max-w-xl gap-4 sm:grid-cols-2"
action="https://formboost.app/f/YOUR_ENDPOINT_ID"
method="POST"
>
<div class="flex flex-col gap-1.5">
<label for="fb-name" class="text-sm font-semibold text-gray-900">Name</label>
<input
type="text"
id="fb-name"
name="name"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
required
autocomplete="name"
>
</div>
<div class="flex flex-col gap-1.5">
<label for="fb-email" class="text-sm font-semibold text-gray-900">Email</label>
<input
type="email"
id="fb-email"
name="email"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
required
autocomplete="email"
>
</div>
<div class="flex flex-col gap-1.5">
<label for="fb-category" class="text-sm font-semibold text-gray-900">What is it about?</label>
<select
id="fb-category"
name="category"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
required
>
<option value="">Choose…</option>
<option value="a-product">A product</option>
<option value="a-service">A service</option>
<option value="billing">Billing</option>
<option value="a-member-of-staff">A member of staff</option>
<option value="something-else">Something else</option>
</select>
</div>
<div class="flex flex-col gap-1.5">
<label for="fb-reference" class="text-sm font-semibold text-gray-900">
Order or reference number
<span class="font-normal text-gray-500">(optional)</span>
</label>
<input
type="text"
id="fb-reference"
name="reference"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
>
</div>
<div class="flex flex-col gap-1.5">
<label for="fb-incident_date" class="text-sm font-semibold text-gray-900">
When did it happen?
<span class="font-normal text-gray-500">(optional)</span>
</label>
<input
type="date"
id="fb-incident_date"
name="incident_date"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
>
</div>
<div class="flex flex-col gap-1.5 sm:col-span-2">
<label for="fb-details" class="text-sm font-semibold text-gray-900">
Tell us what happened
</label>
<textarea
id="fb-details"
name="details"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
required
rows="6"
></textarea>
</div>
<div class="flex flex-col gap-1.5 sm:col-span-2">
<label for="fb-resolution" class="text-sm font-semibold text-gray-900">
What would put this right?
<span class="font-normal text-gray-500">(optional)</span>
</label>
<textarea
id="fb-resolution"
name="resolution"
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2.5 text-gray-900 focus:border-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-600/30"
rows="3"
></textarea>
</div>
<fieldset class="flex flex-col gap-1.5 sm:col-span-2 m-0 min-w-0 border-0 p-0">
<legend class="text-sm font-semibold text-gray-900">
How should we contact you?
<span class="font-normal text-gray-500">(optional)</span>
</legend>
<div class="flex flex-col gap-2">
<label class="flex items-center gap-2 text-gray-800">
<input type="radio" name="contact_method" value="email" class="h-4 w-4 accent-blue-600">
<span>Email</span>
</label>
<label class="flex items-center gap-2 text-gray-800">
<input type="radio" name="contact_method" value="phone" class="h-4 w-4 accent-blue-600">
<span>Phone</span>
</label>
</div>
</fieldset>
<!-- Honeypot: keep it hidden. People never see it; bots fill it in and get flagged as spam. -->
<div
aria-hidden="true"
style="position:absolute;left:-9999px;width:1px;height:1px;overflow:hidden"
>
<label for="fb-honey">Leave this field empty</label>
<input type="text" id="fb-honey" name="_honey" tabindex="-1" autocomplete="off">
</div>
<input type="hidden" name="_subject" value="New complaint">
<button
type="submit"
class="min-h-11 justify-self-start rounded-lg bg-blue-600 px-5 py-2.5 font-semibold text-white hover:bg-blue-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 disabled:opacity-60 sm:col-span-2"
>
Send complaint
</button>
</form>
About this template
A good complaint form makes a complaint easier to resolve, not easier to dismiss. This template asks for the facts you need to investigate — a category, an order or reference number, the date — and then the question most forms skip: what would put this right? Knowing the outcome someone wants is half the resolution.
The tone of the labels matters here. “Tell us what happened” reads better than “Describe the issue”, and none of the fields asks the customer to justify themselves. The reference number is optional, because not every complaint has one and a required field the customer cannot fill in turns a complaint into two.
Say on the page how soon you respond and who will. Complaints are the submissions where a fast acknowledgement matters most — on Starter and above, Auto Reply sends one the moment the form is submitted.
Fields in this template
| Field | Name | Type | Required | Why it is there |
|---|---|---|---|---|
| Name | name | Short text | Yes | Required. |
email | Yes | Required, so you can respond and Auto Reply can acknowledge. | ||
| What is it about? | category | Dropdown | Yes | Required dropdown, for routing to the right person. |
| Order or reference number | reference | Short text | No | Optional — not every complaint has one. |
| When did it happen? | incident_date | Date | No | Optional date picker. |
| Tell us what happened | details | Long text | Yes | Required. The account of what happened. |
| What would put this right? | resolution | Long text | No | Optional, and the most useful answer on the form. |
| How should we contact you? | contact_method | Single choice | No | Optional preference. |
Each name becomes a column in your dashboard and a row in the notification email. The hidden _honey field is the honeypot; _subject sets the email's subject line. Both are reserved fields and never appear in the field table.
Make it yours
- Add a phone field if you prefer to resolve complaints by call — make it optional.
- Rename the categories to your own departments so each complaint reaches its owner.
- Keep the success message human; it is the first response the customer reads.
- If complaints also arrive by phone or in person, add a hidden
channelfield and enter those into the same form, so every complaint is counted in one export.
Before you publish it
- Create a form in the Formboost dashboard and replace
YOUR_ENDPOINT_IDwith its ID. - Pick one variant: HTML posts and reloads, HTML + JS and React send JSON with
fetch()and stay on the page. - For the plain HTML version, add a hidden
_redirectwith anhttps://thank-you page on the same site as the form — anything else falls back to Formboost's page. - Submit it once from the live page and check the submission in your dashboard. Every request is acknowledged, so the dashboard is the real test. Troubleshooting covers the rest.
More form templates
Support ticket form template
Category, urgency, where it happened and how to reproduce it — the details a first reply usually has to ask for.
Feedback form template
A rating, two open questions and the page it came from — short enough that people finish it.
Contact form template
Name, email, a topic and the message — the contact form most sites need, done properly.
All form templates
Every template, in HTML, React and Tailwind.
Put the complaint form live
Free forever: 500 submissions a month across 10 forms, no card.