Forms

The house form pattern: Zod validation with the right timing, baked in.

RangeSelect

A banded numeric filter — headcount, revenue. Nobody holds "mid-market, not enterprise" in their head as a pair of integers, so presets turn a question about numbers into the question actually being asked. Bands are a convenience and not a constraint: Customize takes a typed range and stores it in the same min,max shape as a preset, so nothing downstream can tell them apart. Multi-select, because "51-200 or 201-500" is one audience rather than two searches — and the closed trigger names what is selected, since a control reading "Any" while three bands are set is the same lie as a collapsed section hiding an active filter.

  • RangeSelect

UploadTokenInput

A token field with a file picker folded into it. Typing and uploading fill the same filter, so they belong to the same control — the shape this replaces stacked a heading, a dashed drop zone and two lines of explanation underneath, doubling the height of the busiest filter for an occasional convenience. Parsed values are MERGED with what is already held, so choosing a file after typing does not drop what was typed, and the input is cleared afterwards so the same file can be picked twice (RAG-1819).

  • UploadTokenInput

FilterRail

The surface a column of filters sits on. Before this, a filter column was a bare aside — the fields were fine and the column had no edges, so the rail and the content beside it were the same colour and the eye had to find the boundary from the controls alone. The treatment is borrowed from Sidebar on purpose: bg-sidebar, text-sidebar-foreground and border-sidebar-border are the tokens the app shell already uses to say "this column is chrome" — a filter rail makes the same claim, so it should not invent a second language for it. The body scrolls and the header does not, because a rail long enough to scroll is exactly the one whose clear-all is worth reaching. It ships no icon-button of its own — Button already has a ghost icon variant with a 44px coarse-pointer target, and a local copy would be a forked component with the same job.

  • FilterRail
  • FilterSection
  • FilterField
  • Button

Switch

A setting already in effect, which is why it is not a Checkbox. A checkbox is a member of a set being staged for submission; a switch changes something the moment it moves, and screen readers announce it as on/off rather than checked/unchecked. It still carries a name and value, so it submits through a plain form and survives in the URL. Hit area grows to 44px on coarse pointers while the track stays 16px tall.

  • Switch
Skip leads you already have — on
One lead per company
Disabled

TokenInput

Multiple values as removable tokens. One control covers both "pick from a list" and "type anything": pass items and it suggests them, omit them and it does not. Typed values commit on Enter whether or not they are in the vocabulary — Base UI’s Combobox only selects from items, so the create step is ours, and it matters because a term the provider does not recognise has to be expressible in order to be reported rather than silently impossible. Enter never reaches the surrounding form.

  • TokenInput

Accordion

Collapsible sections that behave as one control. Native <details> stays right for a standalone panel; this is for a SET that coordinates and reports counts — the badge on a closed header is what stops a collapsed section from hiding an active filter. Closed panels keep their fields mounted by default, because an unmounted input does not submit and a collapsed section would otherwise drop its values from the request while still appearing to hold them.

  • Accordion
  • AccordionItem
  • AccordionTrigger
  • AccordionPanel

Checkbox

The system had no checkbox before this — only DropdownMenuCheckboxItem, which is a menu item. Indeterminate is first-class: a select-all over a partial selection is a real third state (ARIA `mixed`), not a checked box with a different glyph. The hit area grows to 44px on coarse pointers while the box stays 16px.

  • Checkbox

FileInput

A file picker that looks like the system. The native input renders OS chrome that cannot be styled ("Choose File / No file chosen"), so the real input stays in the DOM — it is what makes a plain form submit work with no JavaScript — and is visually hidden behind a surface we own. Drag and drop is supported, and writes back into the real input so the file actually posts.

  • FileInput
  • Label

Pick a file or drag one on — the surface shows the name and size, and the clear button appears only once something is selected.

Textarea

Multi-line text, sharing Input’s exact border, radius and focus ring — a form holding both should not look assembled from two libraries. Base UI ships no textarea primitive, so this wraps the native element; it needs no popup or state machine and still participates in a Field through plain id/name. `field-sizing-content` grows the box with the text, and the drag handle is OFF by default because a resizable box inside a card gets dragged over whatever sits beside it.

  • Textarea
  • Label

Multi-step wizard — the flow is a JSON definition

The whole flow is one schema-validated definition object: steps, fields, per-step validation, and branching. The container animates between measured heights and the step content cross-fades through a View Transition, so nothing jumps as you advance — the header never moves and the side pane is the same size on every step (drag the width slider). Choose Team on step 2 and an extra step appears; choose Personal and its values never reach the submitted payload. Progress persists, so Leave & resume brings back the step and the answers. Every advance control is a real submit button inside its own step form.

  • WizardForm
  • FullScreenWizard

Step 1 of 3

About you

How should we address you?

Remounts the wizard from saved progress — the step and answers come back.

useZodForm

The canonical form: a Zod schema drives TanStack Form with validate-on-submit then revalidate-on-change. Live — submit empty, then fix a field.

  • Input
  • Label
  • Button

The house timing, baked into useZodForm: nothing is flagged until the first submit, then every field revalidates as you type — errors clear the moment the input is fixed. Submit empty to see it.