Contact form template
Name, email, a topic and the message — the contact form most sites need, done properly.
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">
<label for="fb-topic" class="fb-label">
What is it about?
<span class="fb-optional">(optional)</span>
</label>
<select id="fb-topic" name="topic" class="fb-input">
<option value="">Choose…</option>
<option value="general-question">General question</option>
<option value="sales">Sales</option>
<option value="support">Support</option>
<option value="press">Press</option>
<option value="something-else">Something else</option>
</select>
</div>
<div class="fb-field">
<label for="fb-message" class="fb-label">Message</label>
<textarea id="fb-message" name="message" class="fb-input" required rows="6"></textarea>
</div>
<!-- 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 message from your website">
<button type="submit" class="fb-button">Send message</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">
<label for="fb-topic" class="fb-label">
What is it about?
<span class="fb-optional">(optional)</span>
</label>
<select id="fb-topic" name="topic" class="fb-input">
<option value="">Choose…</option>
<option value="general-question">General question</option>
<option value="sales">Sales</option>
<option value="support">Support</option>
<option value="press">Press</option>
<option value="something-else">Something else</option>
</select>
</div>
<div class="fb-field">
<label for="fb-message" class="fb-label">Message</label>
<textarea id="fb-message" name="message" class="fb-input" required rows="6"></textarea>
</div>
<!-- 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 message from your website">
<button type="submit" class="fb-button">Send message</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 = "Thanks — your message has been sent.";
} 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 ContactForm() {
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("Thanks — your message has been sent.");
} 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 sm:col-span-2">
<label htmlFor="fb-topic" className="text-sm font-semibold text-gray-900">
What is it about?
<span className="font-normal text-gray-500">(optional)</span>
</label>
<select
id="fb-topic"
name="topic"
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"
>
<option value="">Choose…</option>
<option value="general-question">General question</option>
<option value="sales">Sales</option>
<option value="support">Support</option>
<option value="press">Press</option>
<option value="something-else">Something else</option>
</select>
</div>
<div className="flex flex-col gap-1.5 sm:col-span-2">
<label htmlFor="fb-message" className="text-sm font-semibold text-gray-900">Message</label>
<textarea
id="fb-message"
name="message"
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>
{/* 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 message from your website" />
<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 message
</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 sm:col-span-2">
<label for="fb-topic" class="text-sm font-semibold text-gray-900">
What is it about?
<span class="font-normal text-gray-500">(optional)</span>
</label>
<select
id="fb-topic"
name="topic"
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"
>
<option value="">Choose…</option>
<option value="general-question">General question</option>
<option value="sales">Sales</option>
<option value="support">Support</option>
<option value="press">Press</option>
<option value="something-else">Something else</option>
</select>
</div>
<div class="flex flex-col gap-1.5 sm:col-span-2">
<label for="fb-message" class="text-sm font-semibold text-gray-900">Message</label>
<textarea
id="fb-message"
name="message"
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>
<!-- 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 message from your website">
<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 message
</button>
</form>
About this template
Four fields is the right size for a contact form. Every extra question costs completions, and anything you need beyond a name, an address and the message can be asked in your reply. The one addition is a topic dropdown, which costs the visitor a single click and saves you reading every message to decide who should answer it.
Name and email sit side by side on wider screens and stack on phones. Both carry autocomplete hints, so a phone fills them in with one tap. The email is named email, which is what lets Auto Reply find the visitor's address on Starter and above without any setup.
If you want a walkthrough rather than the code, the HTML contact form guide builds the same form step by step, and there are versions for Tailwind and React.
Fields in this template
| Field | Name | Type | Required | Why it is there |
|---|---|---|---|---|
| Name | name | Short text | Yes | Required, so every reply can start with a name. |
email | Yes | Required. Named email so Auto Reply and the reply-to work without configuration. | ||
| What is it about? | topic | Dropdown | No | Optional dropdown. One click for the visitor, and a way to sort or route messages for you. |
| Message | message | Long text | Yes | Required. Six rows high, so it invites more than a one-liner. |
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
- Rename the topics to the people or teams who answer them, so each message already has an owner.
- Drop the topic dropdown entirely if one person reads everything — a three-field form is fine.
- Send visitors to your own thank-you page with a custom redirect that says how soon you reply.
- If the form sits in a footer on every page, add a hidden
pagefield set fromlocation.pathname, so each message tells you where it was written.
Where the submissions go
- Every message is stored in your dashboard and emailed to you, with the topic in the field table.
- Post new messages to Slack, Discord or Telegram as well — one chat destination per form on the Free plan.
- With the HTML + JS or React version, replying to the notification email answers the visitor directly.
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
Enquiry form template
What they are asking about, how they would like an answer, and the question itself.
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.
All form templates
Every template, in HTML, React and Tailwind.
Put the contact form live
Free forever: 500 submissions a month across 10 forms, no card.