Quote request form
Lead capture with every required submission state.
The demo sends nothing
Submissions on this page are simulated. To see the failure and retry path, put
fail anywhere in the email address — for example you+fail@example.com.
Installation
npx shadcn@latest add http://localhost:3000/r/quote-request-form.jsonMKT UI is not published yet, so this points at your local development server. Set NEXT_PUBLIC_REGISTRY_URL to the deployed origin to show the public command.
Also installs: button, checkbox, input, select, textarea, field, submission-status, use-form-submission.
Usage
"use client";
import { QuoteRequestForm } from "@/components/quote-request-form";
<QuoteRequestForm
services={[
{ value: "emergency", label: "Emergency repair" },
{ value: "water-heater", label: "Water heater" },
]}
onSubmit={async (values) => {
const response = await fetch("/api/quote", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(values),
});
if (!response.ok) {
throw new Error("We couldn't send your request. Please try again.");
}
}}
successMessage="Thanks — we'll be in touch the same business day."
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| services (required) | QuoteServiceOption[] | — | Options for the "What do you need?" select, as `{ value, label }`. |
| onSubmit (required) | (values) => Promise<unknown> | unknown | — | Deliver the request. Throw to show the failure state; the thrown message is shown to the visitor. |
| defaultValues | Partial<QuoteRequestValues> | — | Pre-fill fields, e.g. from campaign URL parameters. |
| submitLabel | string | "Request a quote" | Submit button text. |
| consentLabel | ReactNode | — | Consent wording. Be specific about what is being agreed to. |
| successTitle | ReactNode | "Request received" | Heading on the confirmation panel. |
| successMessage | ReactNode | — | Say what happens next and when. |
| allowAnother | boolean | true | Show a "Send another request" button after success. |
| disabled | boolean | false | Turn the form off — out of hours, paused campaign, booked out. |
| unavailableMessage | ReactNode | — | Explains why the form is disabled. Always pair it with `disabled`. |
| footnote | ReactNode | — | Small print under the submit button, e.g. a privacy note. |
Fields
| Field | Type | Autocomplete | Required |
|---|---|---|---|
| Your name | text | name | Yes |
email | Yes | ||
| Phone | tel | tel | Yes |
| Postcode or suburb | text | postal-code | No |
| What do you need? | select | — | Yes |
| About the job | textarea | — | Yes, min 10 characters |
| Consent | checkbox | — | Yes |
Submission states
| State | What the visitor sees |
|---|---|
| Empty | Blank fields, nothing marked invalid |
| Pre-filled | defaultValues applied |
| Invalid | Inline errors, focus moved to the first invalid field |
| Submitting | Fieldset disabled, "Sending…" announced politely |
| Success | Confirmation panel replaces the form |
| Failure | role="alert" message, button becomes "Try again", input preserved |
| Disabled | Fieldset off, unavailableMessage shown |
Behavior
Validation runs on blur, then live once a field has been touched. Validating every keystroke from the first character tells someone their email is invalid while they are still typing the first letter of it.
A failed submission never clears the form. Nothing in the failure path touches field values, so six lines about a leaking boiler survive a dropped request.
Repeat submissions are blocked by a ref, not by state. Two clicks in the same tick read the same rendered state, so a state-based guard lets the second through. The disabled button is the visible defense; the ref is the one that holds.
Customizing the schema
quoteRequestSchema is exported. To add a field, extend it and widen the values type:
import { quoteRequestSchema } from "@/components/quote-request-form";
const extended = quoteRequestSchema.extend({
preferredTime: z.enum(["morning", "afternoon"]),
});You own the installed file, so adding the matching <Field> is a normal edit.
Server-side validation
Client validation is a convenience for the visitor and is trivially bypassed. Validate again in your handler, and add your own rate limiting, spam protection, and storage. MKT UI does none of those.
Limitations
- Single step. The multistep qualification form (
quote-request-multistep) is not implemented yet. - No file upload field.
- No built-in spam protection — no honeypot, no CAPTCHA integration.
- Field set is fixed. Adding fields means editing your installed copy, which is deliberate: a configurable field schema would be a form builder, and that is explicitly out of scope.
Related
- Forms & booking — the integration contract in full