Sales enquiry
A qualification form that asks only what changes the conversation — and deliberately omits budget.
Live preview
Source
This exact file renders the preview above.
'use client'
import { Field, Fieldset } from '@/components/ui/field'
import { Input, Textarea } from '@/components/ui/input'
import { Checkbox } from '@/components/ui/choice'
import { Select } from '@/components/ui/select'
import { useDemoForm } from '@/hooks/use-demo-form'
import { email, required, url } from '@/lib/validation'
import { FormShell } from './_shell'
/**
* Sales enquiry
*
* A qualification form that stays short by asking only what changes the
* conversation: size, timeline and what they are replacing. Budget is
* deliberately absent — it is the field that most often ends the enquiry.
*/
export default function SalesInquiryForm() {
const form = useDemoForm({
schema: {
name: { validators: [required('Name')] },
email: { validators: [required('Work email'), email()] },
company: { validators: [required('Company')] },
website: { validators: [url()] },
size: { initial: '51-200', validators: [required('Company size')] },
timeline: { initial: 'this-quarter' },
context: {},
interests: { initial: 'false' },
},
})
return (
<FormShell
title="Talk to sales"
description="Tell us enough to make the first call useful."
formRef={form.formRef}
onSubmit={form.handleSubmit}
status={form.status}
serverError={form.serverError}
invalidCount={form.invalidCount}
submitted={form.submitted}
submitLabel="Request a call"
submittingLabel="Sending"
onReset={form.reset}
successTitle="Request received"
width="lg"
>
<div className="grid gap-stack sm:grid-cols-2">
<Field name="name" label="Name" required error={form.error('name')}>
{(field) => <Input {...field} autoComplete="name" {...form.field('name')} />}
</Field>
<Field name="email" label="Work email" required error={form.error('email')}>
{(field) => (
<Input {...field} type="email" autoComplete="email" {...form.field('email')} />
)}
</Field>
</div>
<div className="grid gap-stack sm:grid-cols-2">
<Field name="company" label="Company" required error={form.error('company')}>
{(field) => <Input {...field} autoComplete="organization" {...form.field('company')} />}
</Field>
<Field name="website" label="Website" showOptional error={form.error('website')}>
{(field) => (
<Input
{...field}
inputMode="url"
placeholder="example.com"
{...form.field('website')}
/>
)}
</Field>
</div>
<div className="grid gap-stack sm:grid-cols-2">
<Field name="size" label="Company size" required error={form.error('size')}>
{(field) => (
<Select
{...field}
{...form.field('size')}
options={[
{ value: '1-10', label: '1–10 people' },
{ value: '11-50', label: '11–50 people' },
{ value: '51-200', label: '51–200 people' },
{ value: '201-1000', label: '201–1,000 people' },
{ value: '1000+', label: 'More than 1,000' },
]}
/>
)}
</Field>
<Field name="timeline" label="Timeline">
{(field) => (
<Select
{...field}
{...form.field('timeline')}
options={[
{ value: 'evaluating', label: 'Just evaluating' },
{ value: 'this-quarter', label: 'This quarter' },
{ value: 'next-quarter', label: 'Next quarter' },
{ value: 'urgent', label: 'We need this now' },
]}
/>
)}
</Field>
</div>
<Field
name="context"
label="What are you replacing?"
showOptional
hint="An internal library, another vendor, or nothing yet."
>
{(field) => <Textarea {...field} rows={4} {...form.field('context')} />}
</Field>
<Fieldset legend="Follow-up" name="interests">
<Checkbox
id="sales-newsletter"
name="interests"
label="Send me release notes"
description="Roughly one email a month. Nothing is actually sent in this demo."
checked={form.values.interests === 'true'}
onChange={(event) => form.setValue('interests', String(event.target.checked))}
/>
</Fieldset>
</FormShell>
)
}
components/blocks/forms/sales-inquiry.tsx
'use client'
import { Field, Fieldset } from '@/components/ui/field'
import { Input, Textarea } from '@/components/ui/input'
import { Checkbox } from '@/components/ui/choice'
import { Select } from '@/components/ui/select'
import { useDemoForm } from '@/hooks/use-demo-form'
import { email, required, url } from '@/lib/validation'
import { FormShell } from './_shell'
/**
* Sales enquiry
*
* A qualification form that stays short by asking only what changes the
* conversation: size, timeline and what they are replacing. Budget is
* deliberately absent — it is the field that most often ends the enquiry.
*/
export default function SalesInquiryForm() {
const form = useDemoForm({
schema: {
name: { validators: [required('Name')] },
email: { validators: [required('Work email'), email()] },
company: { validators: [required('Company')] },
website: { validators: [url()] },
size: { initial: '51-200', validators: [required('Company size')] },
timeline: { initial: 'this-quarter' },
context: {},
interests: { initial: 'false' },
},
})
return (
<FormShell
title="Talk to sales"
description="Tell us enough to make the first call useful."
formRef={form.formRef}
onSubmit={form.handleSubmit}
status={form.status}
serverError={form.serverError}
invalidCount={form.invalidCount}
submitted={form.submitted}
submitLabel="Request a call"
submittingLabel="Sending"
onReset={form.reset}
successTitle="Request received"
width="lg"
>
<div className="grid gap-stack sm:grid-cols-2">
<Field name="name" label="Name" required error={form.error('name')}>
{(field) => <Input {...field} autoComplete="name" {...form.field('name')} />}
</Field>
<Field name="email" label="Work email" required error={form.error('email')}>
{(field) => (
<Input {...field} type="email" autoComplete="email" {...form.field('email')} />
)}
</Field>
</div>
<div className="grid gap-stack sm:grid-cols-2">
<Field name="company" label="Company" required error={form.error('company')}>
{(field) => <Input {...field} autoComplete="organization" {...form.field('company')} />}
</Field>
<Field name="website" label="Website" showOptional error={form.error('website')}>
{(field) => (
<Input
{...field}
inputMode="url"
placeholder="example.com"
{...form.field('website')}
/>
)}
</Field>
</div>
<div className="grid gap-stack sm:grid-cols-2">
<Field name="size" label="Company size" required error={form.error('size')}>
{(field) => (
<Select
{...field}
{...form.field('size')}
options={[
{ value: '1-10', label: '1–10 people' },
{ value: '11-50', label: '11–50 people' },
{ value: '51-200', label: '51–200 people' },
{ value: '201-1000', label: '201–1,000 people' },
{ value: '1000+', label: 'More than 1,000' },
]}
/>
)}
</Field>
<Field name="timeline" label="Timeline">
{(field) => (
<Select
{...field}
{...form.field('timeline')}
options={[
{ value: 'evaluating', label: 'Just evaluating' },
{ value: 'this-quarter', label: 'This quarter' },
{ value: 'next-quarter', label: 'Next quarter' },
{ value: 'urgent', label: 'We need this now' },
]}
/>
)}
</Field>
</div>
<Field
name="context"
label="What are you replacing?"
showOptional
hint="An internal library, another vendor, or nothing yet."
>
{(field) => <Textarea {...field} rows={4} {...form.field('context')} />}
</Field>
<Fieldset legend="Follow-up" name="interests">
<Checkbox
id="sales-newsletter"
name="interests"
label="Send me release notes"
description="Roughly one email a month. Nothing is actually sent in this demo."
checked={form.values.interests === 'true'}
onChange={(event) => form.setValue('interests', String(event.target.checked))}
/>
</Fieldset>
</FormShell>
)
}
components/ui/select.tsx
import type { SelectHTMLAttributes } from 'react'
import { ChevronDown } from 'lucide-react'
import { cn } from '@/lib/cn'
import { controlSurface } from './input'
/**
* Select
*
* A styled native `<select>`, on purpose. The native control brings free
* keyboard support, free mobile pickers and free assistive-tech semantics; the
* only thing Foundry adds is the chevron and the shared control chrome.
*
* When the interaction genuinely needs filtering or multi-select, reach for the
* Combobox instead — see `/components/combobox`.
*/
export interface SelectOption {
value: string
label: string
disabled?: boolean
}
export interface SelectProps extends SelectHTMLAttributes<HTMLSelectElement> {
options: SelectOption[]
/** Rendered as a disabled, selected-by-default first option. */
placeholder?: string
selectSize?: 'sm' | 'md' | 'lg'
/** Groups options under `<optgroup>` labels. */
groups?: Array<{ label: string; options: SelectOption[] }>
}
const heights = {
sm: 'h-control-sm text-xs',
md: 'h-control text-sm',
lg: 'h-control-lg text-base',
} as const
export function Select({
options,
groups,
placeholder,
selectSize = 'md',
className,
...props
}: SelectProps) {
return (
<div className="relative flex items-center">
<select
className={cn(
controlSurface,
heights[selectSize],
'cursor-pointer appearance-none py-0 pr-9 pl-3',
className,
)}
defaultValue={props.value === undefined && placeholder ? '' : undefined}
{...props}
>
{placeholder ? (
<option value="" disabled>
{placeholder}
</option>
) : null}
{groups
? groups.map((group) => (
<optgroup key={group.label} label={group.label}>
{group.options.map((option) => (
<option key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
</option>
))}
</optgroup>
))
: options.map((option) => (
<option key={option.value} value={option.value} disabled={option.disabled}>
{option.label}
</option>
))}
</select>
<ChevronDown
className="pointer-events-none absolute right-3 size-4 text-ink-subtle"
aria-hidden="true"
/>
</div>
)
}
Demo source — adapt to your project. Foundry is not published as a package.
Usage
Short by design: size, timeline and what they are replacing. Budget is absent because it is the field that most often ends the enquiry before it starts.
- Website is optional and validated loosely — a URL rule that rejects “example.com” loses leads.
- The opt-in is unchecked by default, which is both correct and, in many jurisdictions, required.
Variants and states
Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.
- Company and size
- Timeline
- Replacement context
- Newsletter opt-in
Accessibility
- Optional marking
- Optional fields are labelled, so required ones need no asterisk storm.
- Grouped opt-in
- The newsletter checkbox sits in its own Fieldset with a legend.
Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.
Related
All formsQuote request
A slider-driven live estimate, shown as a range with an explicit caveat.
intermediate4 variantsEnterprise hero
Two equal-weight actions and an assurance panel carrying compliance facts.
starter3 variantsPartnership enquiry
Multi-select partnership types as selectable cards, plus a substantive proposal field.
starter3 variants