Resolved: light

Forms (advanced inputs)

The richer, JavaScript-backed form controls from @phauna/ds — the ones the plain native Forms page can't express. Each is a thin token-driven wrapper over a Base UI primitive (checkbox / radio / switch / slider / number-field / combobox / select) styled off data-attributes, or — for the date field — a styled native input. They are CLIENT components (they own focus, keyboard and open/close state), so every controlled demo below lives in one colocated 'use client' island; this page is a server component that just mounts them. Import each from its correct subpath — the interactive controls from @phauna/ds/overlays, FileUpload from @phauna/ds/files. Flip the theme / brand / density in the top bar to confirm every control re-skins from the semantic tokens; tab into any field to see the shared focus ring.

where they live
Combobox, Checkbox(Group), Radio(Group), Switch, Slider, NumberInput, SearchInput, Select and DatePicker import from @phauna/ds/overlays; FileUpload from @phauna/ds/files.
client boundary
All are client components. Render them from a server page only inside a 'use client' island (here ./AdvancedFormsDemo) — never call useState in this server file.
controlled vs uncontrolled
Base-UI controls take value/checked + an onChange callback (controlled) OR defaultValue/defaultChecked (uncontrolled). The demos use whichever reads clearest per control.
naming
The brief's "NumberField" / "DateInput" are exported as NumberInput / DatePicker. (The index barrel's DateInput is a date-formatting TYPE, not a control.)
states shown
Each control is shown across the states it actually supports — default, filled, disabled, and error/invalid where the wrapper exposes it (NumberInput, SearchInput, DatePicker take invalid).
accessibility
Sliders / NumberInputs / Selects / Comboboxes have no built-in visible label, so each demo passes aria-label (or label, or a wrapping <label>/<legend>). Never rely on the control's shape alone.
tsx
"use client";
import { useState } from "react";
import { Combobox, type ComboboxOption } from "@savethemarshalldogs/design-system/overlays";

const OPTIONS: ComboboxOption[] = [
  { value: "intake", label: "Intake & triage" },
  { value: "transport", label: "Transport" },
];
const [team, setTeam] = useState<ComboboxOption | null>(null);

<Combobox
  items={OPTIONS}
  value={team}
  onValueChange={setTeam}
  placeholder="Search teams…"
  aria-label="Team"
/>

Combobox (searchable select)

A single-select with a type-to-filter text input — reach for it over Select once the option list gets long enough to want search. items is a {value,label}[]; the selected value is the whole option object (or null), so onValueChange hands you back the picked option. A custom emptyText covers the no-match case.

selected: Transport

default — type to filter; value is the option object
empty — placeholder shown, no selection
disabled — not focusable, dimmed
custom empty text — shown when no item matches the query
items
ComboboxOption[] — { value: string; label: string }. Rendered into the filterable listbox.
value / defaultValue
The selected OPTION object (ComboboxOption) or null — not just its value string. Use value for controlled, defaultValue for uncontrolled.
onValueChange
(value: ComboboxOption | null) => void. Fires with the chosen option (or null when cleared).
placeholder / emptyText
placeholder is the empty input hint (default “Search…”); emptyText shows in the popup when nothing matches (default “No matches.”).
disabled
Passes through to the Base UI root — the input is non-focusable and dimmed.
label it
The wrapper renders only an input (no label). Pass aria-label, or wrap it in a <label>, so the control is named.

Checkbox & CheckboxGroup

Checkbox is a single boolean with an optional label + hint (label makes the whole row the click target). CheckboxGroup coordinates several checkboxes: its value is the array of CHECKED CHILD NAMES, so each child Checkbox needs a name and the group owns the list. row lays them out horizontally.

single — default / checked / disabled / disabled-checked

checked: true

controlled single — checked + onCheckedChange

value: [transport, foster]

CheckboxGroup — value is the array of checked child names
CheckboxGroup row — horizontal layout (row), one disabled
Checkbox label / hint
label renders beside the box and makes the row clickable; hint is muted sub-text under the label. Omit label for a bare box (then it takes className directly).
Checkbox checked / defaultChecked
Base UI props: checked + onCheckedChange(next: boolean) for controlled, defaultChecked for uncontrolled. Also disabled, required, name.
CheckboxGroup value
string[] of the checked children's name props (value/defaultValue). onValueChange(next: string[]) fires with the new set — so always give each child Checkbox a name.
CheckboxGroup row
boolean — lay the children out horizontally (choice-group--row) instead of stacked.
disabled
Set on a child to disable just that option, or on the group to disable them all.

Radio & RadioGroup

A single-choice set. Radios are only meaningful inside a RadioGroup — the group holds the selected value and each Radio carries the value it represents (plus an optional label + hint). Use it instead of a Select for a small, always-visible set of mutually-exclusive options. row lays them out horizontally.

value: email

controlled group — value + onValueChange (vertical)
uncontrolled row group — defaultValue, horizontal (row)
whole group disabled — every option inert
Radio value
Required — the value this option contributes to the group. label + hint mirror Checkbox; disabled disables just this option.
RadioGroup value / defaultValue
The currently-selected value (controlled) or initial value (uncontrolled). onValueChange(next) fires with the newly-selected value.
RadioGroup row
boolean — horizontal layout (e.g. a size picker). Default is stacked.
disabled
Set on the group to disable the whole set, or on an individual Radio for one inert option.
name it
Pass aria-label (or wrap in a <fieldset>/<legend>) on the RadioGroup — the set needs an accessible name.

Switch

A binary on/off toggle for an immediate setting (vs a checkbox, which states a fact to be submitted). Passing a label renders the whole label+switch row as one click target; without it you get a bare switch (give it an aria-label). On is bg-accent with the thumb shifted right — but the label, never the colour, carries the meaning.

bare + labelled / disabled states

checked: true

controlled — checked + onCheckedChange
checked / defaultChecked
Base UI props: checked + onCheckedChange(next: boolean) for controlled, defaultChecked for uncontrolled.
label
Optional — renders text beside the switch and wraps both in a clickable <label> (switch-row). Omit it for a bare switch.
disabled
Dims the switch and ignores interaction.
Switch vs Toggle vs Checkbox
Switch = an instant on/off setting. The native Toggle (src/components/ui) is the older app switch. Checkbox = a value you submit with a form.
name a bare switch
Without a label, pass aria-label so screen readers announce what the switch controls.

Slider

A draggable range for a bounded numeric value (a radius, a percentage). min / max / step bound and quantise it; showValue prints the live value beside the track. It has no visible label of its own, so always pass aria-label. Pair the live value text when the exact number matters.

15

value: 15 mi

default — 0–100, showValue prints the live thumb value
without showValue / disabled
value / defaultValue
number — controlled (value + onValueChange(next)) or uncontrolled (defaultValue).
min / max / step
Bound and quantise the value (default 0–100, step 1). step=5 snaps to fives.
showValue
boolean — render the current value (slider__value) beside the track. Off by default.
onValueChange
Fires continuously while dragging. (onValueCommitted fires once on release — use it if continuous updates are costly.)
disabled
Locks the thumb and dims the track.
aria-label
Required for a11y — the slider has no intrinsic label.

NumberField (NumberInput)

A numeric entry with −/+ stepper buttons, typed input, and optional prefix/suffix affixes (a $ or a unit). min/max/step clamp and step the value; the value is number | null (null when the field is cleared). Exported as NumberInput. The text input takes its own props via inputProps — that's where the aria-label goes.

value: 3

default — controlled value + onValueChange, step buttons
mi
suffix — a trailing unit affix
$
prefix — a leading affix (e.g. currency)
invalid + disabled
value / defaultValue
number | null — controlled (value + onValueChange) or uncontrolled (defaultValue). onValueChange hands back number | null (null = cleared).
min / max / step
Clamp and step the value; the −/+ buttons and arrow keys move by step (default 1; step accepts a number or 'any').
prefix / suffix
ReactNode affixes rendered inside the field (e.g. prefix="$", suffix="mi").
invalid
boolean — sets data-invalid on the group for the error styling. Pair with your own message + aria wiring.
inputProps
Props forwarded to the inner <input> — this is where aria-label, inputMode, placeholder etc. go (the stepper buttons already carry their own labels).
disabled
Disables the input and both stepper buttons.

SearchInput

A controlled search box with a leading magnifier and a clear (×) button that appears once there's a value. It is fully controlled — value and onValueChange are required (the wrapper calls onValueChange('') when the × is clicked). Use it for the live filter above a list; the faceted FilterBar is the heavier, multi-condition cousin.

query: “transport

filled — the clear (×) button shows when value is non-empty
empty — placeholder only, no clear button
invalid — data-invalid styling
disabled — not editable
value / onValueChange
Both REQUIRED — it's a controlled input. onValueChange(next: string) fires on every keystroke and on clear (with "").
clear button
Auto-shown when value is non-empty; clicking it calls onValueChange('').
invalid
boolean — sets data-invalid / aria-invalid for the error treatment.
placeholder
Default “Search…”. Override per use.
disabled
Dims the field and blocks input.
label it
Pass aria-label (the input is type=search with no visible label of its own).

Select (accessible)

The Base-UI Select — a fully-styled, keyboard-accessible replacement for a native <select> that matches the rest of the system (the native <Select> on the Forms page is the lighter, zero-JS option). items is a {value,label,disabled?}[]; name it with label or aria-label. Reach for the Combobox instead once the list is long enough to want search.

value: northeast

default — controlled value + onValueChange
placeholder — no default selection
with a disabled option
disabled — whole control inert
items
SelectOption[] — { value: string; label: ReactNode; disabled?: boolean }. A per-item disabled greys just that option.
value / defaultValue
The selected value string — controlled (value + onValueChange(value)) or uncontrolled (defaultValue).
placeholder
Trigger text shown when nothing is selected.
label / aria-label
Required for a11y unless an external <label> points at it — label sets the trigger's accessible name.
disabled / name
disabled inerts the whole control; name identifies it on form submit.
Select vs native vs Combobox
Use this for a styled, bounded set; the native <Select> for the lightest zero-JS option; the Combobox once you need search.

DateInput (DatePicker)

A styled NATIVE date input (an <input type='date'>, exported as DatePicker) — you get the OS date picker, keyboard entry and locale formatting for free, with the field skinned to match the rest of the system. Value is the ISO yyyy-mm-dd string; all native date attributes (value/defaultValue/min/max/disabled) pass straight through, plus an invalid flag.

value: 2026-07-04

controlled — value + onChange (ISO yyyy-mm-dd)
empty — no default; the native placeholder shows
bounded — min / max clamp the pickable range
invalid + disabled
value / defaultValue
ISO yyyy-mm-dd string. Controlled via value + onChange (a native input event — read e.target.value); defaultValue for uncontrolled.
min / max
Native date bounds — clamp the pickable range (also yyyy-mm-dd strings).
invalid
boolean — sets aria-invalid for the error styling.
disabled
Standard native disable.
why native
No date-fns / no JS calendar component — the platform picker is accessible, localised and free. DatePicker is the styling-only wrapper.
label it
Pass aria-label or bind a <label htmlFor> — the input has no built-in label.

FileUpload

A single-file picker from @phauna/ds/files — a labelled trigger that opens the OS file dialog and calls onFile with the chosen File (or null when cleared). The DS owns the UI; the APP owns what to do with the File (validate / upload). For multi-file drag-and-drop with progress, the files module also ships Dropzone / Uploader / FileBrowser.

picked:

default — onFile receives the picked File (or null)
accept — narrows the OS file dialog (images only)
custom label content
disabled — picker is inert
label
ReactNode — the trigger's label / call-to-action.
onFile
(file: File | null) => void. Fires with the picked File, or null when the selection is cleared. The app decides what happens next (no upload is wired in the DS).
accept
Standard accept string (e.g. image/*, application/pdf) — narrows the OS file dialog.
disabled
Inerts the picker.
single vs many
FileUpload is one file. For multi-file DnD + per-file progress use Uploader / Dropzone / FileBrowser from @phauna/ds/files (the app supplies the endpoint).

Putting it together — a volunteer availability form

A realistic composition wiring several advanced controls into one onboarding step: a Combobox for the team, a Select for the region, a CheckboxGroup of skills, a Slider for travel radius, a NumberInput for foster spots, a DatePicker for the start date, a RadioGroup for contact preference, and a Switch for SMS. Every control is named via a wrapping label / fieldset legend / aria-label, and the footer follows the primary + ghost pairing. Presentational only — the submit is prevented.

Skills you can offer
Travel radius
20
Preferred contact
availability form — Combobox + Select + CheckboxGroup + Slider + NumberInput + DatePicker + RadioGroup + Switch
labelling
Each control sits in a wrapping <label> (for the single inputs) or a <fieldset> with a <legend> (for the groups), so every control has a visible, associated name.
controlled state
The island holds one piece of state per control; this server page never touches state — it just mounts the island.
footer
Right-aligned primary (Save) + ghost (Cancel) — the same pairing rule as the Buttons page.
no submit
onSubmit is preventDefault'd — the styleguide never posts. Wire a real handler in the feature page.