Project inquiry form
Qualifying enquiry form: services, budget, timeline, and context.
The demo sends nothing
Submissions on this page are simulated. To see the failure and retry path, put
fail anywhere in the email address.
Installation
npx shadcn@latest add http://localhost:3000/r/project-inquiry-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 { ProjectInquiryForm } from "@/components/project-inquiry-form";
<ProjectInquiryForm
services={[
{ value: "strategy", label: "Strategy", note: "Positioning and messaging" },
{ value: "build", label: "Build", note: "Web and product" },
]}
budgets={[
{ value: "10-25k", label: "$10k – $25k" },
{ value: "25-50k", label: "$25k – $50k" },
]}
timelines={[
{ value: "asap", label: "As soon as possible" },
{ value: "exploring", label: "Just exploring" },
]}
onSubmit={async (values) => {
const response = await fetch("/api/inquiry", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(values),
});
if (!response.ok) throw new Error("We couldn't send your inquiry. Please try again.");
}}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
| services (required) | InquiryOption[] | — | What the visitor can ask for. Rendered as a checkbox group. |
| budgets (required) | InquiryOption[] | — | Budget bands. |
| timelines (required) | InquiryOption[] | — | When they need it done. |
| onSubmit (required) | (values) => Promise<unknown> | unknown | — | Deliver the enquiry. Throw to show the failure state. |
| defaultValues | Partial<ProjectInquiryValues> | — | Pre-fill, e.g. from a campaign URL. |
| submitLabel | string | "Send project inquiry" | Submit button text. |
| successTitle | ReactNode | "Inquiry received" | Heading on the confirmation. |
| successMessage | ReactNode | — | Say what happens next and when. |
| disabled | boolean | false | Turn the form off. Pair with `unavailableMessage`. |
| footnote | ReactNode | — | Small print under the submit button. |
Fields
| Field | Type | Required |
|---|---|---|
| Your name | text | Yes |
| Work email | Yes | |
| Company | text | No |
| What do you need help with? | checkbox group | At least one |
| Budget | select | Yes |
| Timeline | select | Yes |
| About the project | textarea | Yes, min 20 characters |
| Consent | checkbox | Yes |
Behaviour
Budget and timeline are bands, not free text. A deliberate trade: a visitor who will not type a number will still pick a range, and a range is enough to route the enquiry to the right person.
The service checkboxes are a real fieldset with a legend. Without it a screen
reader announces four unrelated checkboxes and no question. The "choose at least one"
error attaches to the group, not to any individual box, because that is where the
failure actually is.
All the submission-state behaviour from
quote-request-form applies here too: validation
on blur then live, duplicate submits blocked by a ref, and input preserved on
failure.
Limitations
- Single step. The multistep qualification variant is not implemented.
- No file upload, so no brief or brand-guideline attachments.
- No spam protection — no honeypot, no CAPTCHA.
- Field set is fixed. Adding fields means editing your installed copy, which is deliberate: a configurable field schema would be a form builder.