With @k8ordo/ui
@k8ordo/form does not depend on @k8ordo/ui; instead, @k8ordo/ui’s fields are built to take what formFields derives as it is. This page shows how each component fits, and the few exceptions.
On this page
Pair it with FormControl
Give FormControl only what it uses: the label, the error, invalid and required. It creates the id and the aria-* links itself.
talk-form.tsxconst title = form.field('title');
<FormControl
errorText={title.error}
invalid={title.invalid}
label="Title"
required={title.required}
renderInput={(props) => <TextField {...props} {...title.input} />}
/>Spread input after the props FormControl hands over. Nothing has to be taken out first: TextField renders the date and time types too, and Textarea drops the type a <textarea> does not have.
Every field keeps its value in the DOM, so a reset restores the submitted values and isDirty reads them. A value a component changes in code — a stepper, a chosen option — arrives as an input event, the way typing does.
Components by schema
For each kind of schema, the components that fit and what to pass besides {...props} {...input}:
z.string(),z.email(),z.url(),z.iso.*():TextFieldorTextarea, with nothing else to pass.- A password:
PasswordInput, whose show/hide toggle keeps working under a spreadtype. z.coerce.number():NumberFieldorSlider, which take string bounds andstep="any"as they are.z.enum([…]):Select,RadioorRadioCard. Passoptions, andlabelAs="legend"onFormControlfor radios.z.boolean(),z.literal(true):CheckboxorSwitch, with alabeland noFormControl.z.array(z.enum([…])):CheckboxGroup.Root,CheckboxCardorAutocomplete, withdefaultValuefromstate.values.z.file():FileField.Root, holding aFileField.Triggerand aFileField.ItemList.
Choice components
Radio and RadioCard take the spread as it is. Unlike a hand-written radio, they read defaultValue as the selected option and put required on every radio.
For required to mean anything on a Select, put { value: '', label: '…' } first in options. A <select> without an empty option always has a choice selected.
A checkbox group echoes as an array. input carries only the name, so pass what was checked back as defaultValue.
tags-field.tsxconst tags = form.field('tags');
const checked = state.values?.tags;
<FormControl
errorText={tags.error}
invalid={tags.invalid}
label="Tags"
labelAs="legend"
renderInput={(props) => (
<CheckboxCard
{...props}
{...tags.input}
defaultValue={Array.isArray(checked) ? checked : []}
options={options}
/>
)}
/>minChecked reaches Autocomplete too. It submits through a hidden <select multiple> that is there even with nothing selected, so the rule has an element to mark, and focus sent there after a failure goes on to the text input.
Components to watch
Pitfall
A spread value does not reach Checkbox, so a z.stringbool() box drawn with it submits the browser’s on. The default z.stringbool() reads that as true, but a custom truthy, or the URL state, does not share it. Render a plain <input {...field.input} /> there.
NumberField renders type="text" to format and step the value, so the browser does not check min and max. It reports an out-of-range value itself with setCustomValidity, in @k8ordo/ui’s wording rather than zod’s — the one place the client’s text is not zod’s own.
A regex on a field drawn as a Textarea reaches the markup as pattern, but a <textarea> ignores it. The check runs on the server only, and dropped cannot list it, since the derivation does not know which element renders the field.
FileField submits what FileField.ItemList shows: removing a file there removes it from the submission, and a reset empties the list.
Show the form-level error with Alert
form.formError fits Alert, which passes id and tabIndex through to its element, so spreading form.formError.props lets it take focus.
talk-form.tsx{form.formError.message !== undefined && (
<Alert
{...form.formError.props}
message={form.formError.message}
tone="error"
/>
)}Alert has role="alert", so a screen reader may read the message twice: once when it appears, once when focus lands on it. It is the same trade an error summary with an alert role makes.