MKT UI
Sections

Quote request form

Lead capture with every required submission state.

Open preview (opens in a new tab)

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.json

MKT 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

PropTypeDefaultDescription
services (required)QuoteServiceOption[]Options for the "What do you need?" select, as `{ value, label }`.
onSubmit (required)(values) => Promise<unknown> | unknownDeliver the request. Throw to show the failure state; the thrown message is shown to the visitor.
defaultValuesPartial<QuoteRequestValues>Pre-fill fields, e.g. from campaign URL parameters.
submitLabelstring"Request a quote"Submit button text.
consentLabelReactNodeConsent wording. Be specific about what is being agreed to.
successTitleReactNode"Request received"Heading on the confirmation panel.
successMessageReactNodeSay what happens next and when.
allowAnotherbooleantrueShow a "Send another request" button after success.
disabledbooleanfalseTurn the form off — out of hours, paused campaign, booked out.
unavailableMessageReactNodeExplains why the form is disabled. Always pair it with `disabled`.
footnoteReactNodeSmall print under the submit button, e.g. a privacy note.

Fields

FieldTypeAutocompleteRequired
Your nametextnameYes
EmailemailemailYes
PhoneteltelYes
Postcode or suburbtextpostal-codeNo
What do you need?selectYes
About the jobtextareaYes, min 10 characters
ConsentcheckboxYes

Submission states

StateWhat the visitor sees
EmptyBlank fields, nothing marked invalid
Pre-filleddefaultValues applied
InvalidInline errors, focus moved to the first invalid field
SubmittingFieldset disabled, "Sending…" announced politely
SuccessConfirmation panel replaces the form
Failurerole="alert" message, button becomes "Try again", input preserved
DisabledFieldset 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.

On this page