API reference
useForm<M, R>()
Section titled “useForm<M, R>()”Creates a self-contained form store.
| Property | Type | Description |
|---|---|---|
Form |
Component | Renders the form element; accepts the same props as Form |
state |
FormState<M> |
Reactive state object (see State API) |
store |
FormStore<M> |
Raw [state, mutations] tuple (see Mutations) |
Use useForm when you need to read field values or validity outside the form tree, or to call
mutations such as reset. Use Form directly when you only need a submit handler.
Called from inside an existing form context — under a <Form> or a FormContextProvider —
useForm returns that form’s store instead of creating a new one.
Pass a Standard Schema-compatible schema to infer values without a form generic:
const form = useForm({ schema: loginSchema });
<form.Form onSubmit={(values) => values.email}>{/* fields */}</form.Form>;A self-contained form that creates its own internal store.
<Form<LoginValues> onSubmit={handleSubmit} errors={['Server error']} isLoading={isPageLoading}> {/* fields */}</Form>
<Form schema={loginSchema} onSubmit={(values) => values.email}> {/* values inferred from loginSchema */}</Form>| Prop | Type | Description |
|---|---|---|
onSubmit |
(values: M) => void | Promise<void> |
Submit handler |
schema |
StandardSchemaV1 |
Optional Standard Schema-compatible validator; successful output is passed to onSubmit |
children |
JSX.Element |
Field components and submit buttons |
errors |
string[] |
Form-level errors to display |
isLoading |
boolean |
Disables registered fields and makes submit actions unavailable while true |
isProcessing |
boolean |
Marks the form in flight for work of your own; OR’d with the form’s own submit state |
processingLabel |
string |
Screen-reader announcement while a submit is in flight; defaults to 'Submitting…', '' to disable |
loadingLabel |
string |
Screen-reader announcement while form data is loading; defaults to 'Loading…', '' to disable |
className |
string |
CSS class on the form element |
align |
'left' | 'center' |
Button alignment, defaults to 'left' |
fullWidthButtons |
boolean |
Stretch buttons to full width |
createForm<M>()
Section titled “createForm<M>()”Form and the field components are each generic over the form’s value type (M), and field
components are also generic over their own name (N) — but JSX can’t carry a type parameter from
<Form<M>> down into a sibling field’s own generic inference, so every element is type-checked in
isolation. Spelling out <M, 'name'> on Form and every field works (see InputField below) but
gets repetitive for a component that only ever renders one form. Call createForm<M>() once instead
to bind M for Form and all field components together:
const { Form, InputField, PasswordField } = createForm<LoginValues>();
<Form onSubmit={(values) => values.email}> <InputField name='email' type='email' label='Email' required /> <PasswordField name='password' label='Password' required minLength={8} /></Form>;onSubmit’s values, name='bogus', and a self-referencing match are all checked against M,
exactly as if you had written <Form<LoginValues>> and <InputField<LoginValues, 'email'>> at each
call site. Nested names are checked too: with M = { address: { city: string } }, name='address.city'
compiles and its value is typed string, while name='address.town' is an error. The returned
components are the real Form, InputField, PasswordField, TextAreaField, CheckboxField,
SelectField, RadioGroup, NumberField, DateField, and FileField — createForm only fixes M
at the type level, it does not wrap or change their behavior.
Pass a Standard Schema-compatible schema instead of a type argument to infer M from it, the same as
useForm({ schema }):
const { Form, InputField, PasswordField } = createForm({ schema: loginSchema });
<Form onSubmit={(values) => values.email}> <InputField name='email' type='email' label='Email' required /> <PasswordField name='password' label='Password' required /></Form>;Fields are typed against the schema’s input (what the DOM gives you); onSubmit receives the
schema’s output, which can differ for a transform/coercion schema. The schema also becomes Form’s
default, so it does not need to be repeated as a schema prop — though an individual <Form schema={other}> call can still override it, same as useForm.
If several field groups need to share one Form/store — a multi-step form, for example — call
createFields<M>() instead to bind just the field components, and wire the shared store up with
useForm/FormContextProvider (see below).
createFields<M>()
Section titled “createFields<M>()”The field-only half of createForm<M>(), for when the fields don’t own their Form:
const { InputField, PasswordField } = createFields<LoginValues>();
<InputField name='email' type='email' label='Email' required /><PasswordField name='password' label='Password' required minLength={8} />InputField
Section titled “InputField”Renders a labeled <input>, defaulting to type="text".
<InputField<M, 'email'> name='email' type='email' label='Email address' defaultValue='user@example.com' required />
<InputField<M, 'slug'> name='slug' label='Slug' pattern={/^[a-z0-9-]+$/} />Prefer type='email' over a hand-written pattern — it drives the on-screen keyboard and is
format-checked against the HTML specification. See
validation.
In the table, V is FieldPathValue<M, N>, the value type at the field’s path.
| Prop | Type | Description |
|---|---|---|
name |
FieldPath<M> |
A key or dotted path in the form value type, such as email or address.city |
label |
string |
Visible label text |
defaultValue |
V |
Initial value |
disabled |
boolean |
Disables the input |
readonly |
boolean |
Makes the input read-only |
parse |
(raw: string) => V |
Convert DOM string to typed value |
format |
(val: V) => string |
Convert typed value back to display string |
validator |
CustomValidator<M, N> |
Custom validation function |
required, minLength, maxLength, pattern, min, max, match, step |
Constraint props | Built-in validation constraints |
type |
string |
Standard input type; email and url are also format-checked |
All standard HTML input attributes are also accepted.
PasswordField
Section titled “PasswordField”Same props as InputField. Renders <input type="password">.
TextAreaField
Section titled “TextAreaField”Same props as InputField, except type. Renders a <textarea>.
<TextAreaField name='bio' label='Bio' maxLength={500} />CheckboxField
Section titled “CheckboxField”Renders a labeled checkbox. The field value in form state is a boolean.
<CheckboxField name='acceptTerms' label='I accept the terms' required />| Prop | Type | Description |
|---|---|---|
name |
FieldPath<M> |
Field name or dotted path |
label |
string |
Label text |
defaultChecked |
boolean |
Initial checked state |
disabled |
boolean |
Disables the checkbox |
required |
boolean |
Field must be true to be valid |
validator |
CustomValidator<M, N> |
Custom validation function |
SelectField
Section titled “SelectField”Renders a native, labeled <select> with the same form-state, disabled, and
error treatment as the other fields. Supply standard <option> children; native
select behavior is retained for keyboard and platform accessibility.
<SelectField name='timezone' label='Time zone'> <option value='America/Los_Angeles'>Pacific time</option> <option value='America/New_York'>Eastern time</option></SelectField>It accepts standard select attributes and the shared field props, including
required, disabled, defaultValue, parse, format, and validator.
RadioGroup
Section titled “RadioGroup”Renders one native radio group backed by a single field value. Its options
array supplies each option’s stored value and accessible label.
<RadioGroup name='delivery' label='Notification delivery' options={[ { value: 'email', label: 'Email' }, { value: 'push', label: 'Push notification' } ]}/>Each option is { value: string; label: string; disabled?: boolean }. It accepts the shared field
props except type and value.
NumberField
Section titled “NumberField”Same props as InputField, except type. Renders <input type="number">. The stored value is the
input’s string, not a number, so clearing an optional field stores '' rather than 0; pass parse
to store numbers and choose how empty is represented. See the field palette.
<NumberField name='quantity' label='Quantity' min={1} step={1} />DateField
Section titled “DateField”Same props as InputField, except type. Renders <input type="date">. Values stay in the
browser’s yyyy-mm-dd format, the same format min, max, and step use; parse and format
convert to and from Date objects if you want those instead.
<DateField name='startsOn' label='Start date' min='2026-01-01' max='2026-12-31' />FileField
Section titled “FileField”Renders <input type="file"> and stores the selected FileList. Clearing the selection stores
undefined, so required rejects it, and reset() or resetField() clear the native control.
Browsers do not allow setting a file input’s value, so it takes no value, parse, or format
props.
<FileField name='avatar' label='Profile photo' accept='image/*' required />SubmitButton
Section titled “SubmitButton”Renders a submit button. It stays enabled while the form is invalid, and marks itself
aria-disabled while a submit is in flight.
Submitting an invalid form reveals every field’s errors and moves focus to the first invalid field, rather than silently doing nothing. A disabled submit button would leave the tab order entirely and give no way to discover what is missing, so validity gates the result of the submit, not access to it.
An in-flight submit is marked with aria-disabled rather than the disabled attribute for a
related reason: the user who pressed the button is focused on it, and disabling a focused element
removes it from the tab order, so the browser drops focus to <body> and nothing restores it when
the submit settles. aria-disabled announces the same unavailability while keeping focus and tab
position; the component blocks activation itself, since aria-disabled is advisory.
While in flight the button also shows a spinner. It is decorative (aria-hidden), because the form’s
live region does the announcing, and it is positioned inside the button’s own padding so the button
neither grows nor re-centers its label mid-submit. Under prefers-reduced-motion it renders as a
static dot.
In a form with more than one submit button, the spinner appears only on the button that was
pressed — every other submit button is still marked aria-disabled, because a second submit really
is unavailable while the first is running, but only one action is actually running. When nothing
pressed a button — a caller-declared <Form isProcessing>, or a programmatic submit — every submit
button shows the spinner, since there is no one action to attribute it to.
Pass isDisabled when the action is genuinely unavailable for a reason of your own — it renders a
real disabled attribute, unlike the in-flight state. It is read as a value, not as a presence, so
binding it to a signal works the way you would expect:
<SubmitButton isDisabled={!termsAccepted()}>Sign up</SubmitButton>While that reads false the button behaves exactly as if the prop were absent, in-flight spinner
included; while it reads true the button is hard-disabled and gets no in-flight treatment, since a
disabled button cannot be the one submitting.
<SubmitButton>Log in</SubmitButton>
<SubmitButton name='saveDraft'>Save draft</SubmitButton><SubmitButton name='publish'>Publish</SubmitButton>Named submit buttons select a handler from an object-style onSubmit map, for example
onSubmit={{ saveDraft, publish }}.
When onSubmit is a map of more than one handler, each SubmitButton needs a name matching one
of its keys — that name is what selects the handler. A button without one selects nothing, so the
submit does nothing at all; the library logs a console warning naming the available handlers rather
than failing silently. A single-handler map needs no name, since there is nothing to disambiguate,
and a plain function onSubmit never needs one.
| Prop | Type | Description |
|---|---|---|
children |
JSX.Element |
Button label |
isDisabled |
boolean |
Hard-disable the button with a real disabled |
isFullWidth |
boolean |
Stretch to container width |
name |
string |
Optional field name for multi-button forms |
variant |
'primary' | 'approve' |
Visual variant; approve renders type="button" |
FieldArray
Section titled “FieldArray”Renders one row per item of a repeating section, such as line items. Each row’s render function
receives field components scoped to that row, typed against the row’s own item type and registered
under name.<index> at runtime, plus the row’s seed value and a remove callback.
<FieldArray<LineItem> name='items' defaultValue={[emptyItem]} helpersRef={(helpers) => (items = helpers)}> {(fields, item, remove) => ( <> <fields.InputField name='description' label='Description' defaultValue={item.description} /> <SubmitButton variant='approve' onClick={remove}> Remove </SubmitButton> </> )}</FieldArray>| Prop | Type | Description |
|---|---|---|
name |
string |
The array’s field name; rows register as name.0.*, name.1.*, … |
defaultValue |
Item[] |
Initial rows |
helpersRef |
(helpers: FieldArrayHelpers) => void |
Receives append, remove, move, and the other helpers once |
rowClass |
string |
Extra class on each row’s wrapper, beside sf-field-array-row |
children |
(fields, item, remove) => JSX.Element |
Renders one row |
The scoped field components are InputField and PasswordField. Like any field, FieldArray must be
rendered from a component inside <Form>, not from the component that renders the <Form> itself.
See Field arrays.
useFieldArray(name, defaultValue?)
Section titled “useFieldArray(name, defaultValue?)”The hook behind FieldArray, for building your own row layout. Returns [items, helpers]: items
is an accessor of { key, defaultValue } rows, where key is a stable identity for keying <For>
that survives reordering. helpers has append(value), prepend(value), insert(index, value),
remove(index), move(from, to), swap(a, b), and pathAt(index), which takes a row’s index
accessor (from <For>) and returns an accessor of that row’s field-name prefix, such as items.2.
Call it from a component rendered inside <Form>, since it reads the form’s context.
createScopedFields<Item>(base)
Section titled “createScopedFields<Item>(base)”Returns InputField and PasswordField components that register under ${base()}.<name> and are
typed against Item — what FieldArray hands each row. Pair it with useFieldArray’s pathAt for a
custom row layout. match is scoped the same way, so a row’s match='password' refers to that row’s
own password field.
FormContextProvider
Section titled “FormContextProvider”Provides an existing store to the tree below it. A <Form> or useForm() rendered inside it reuses
that store instead of creating its own, so a self-contained form component can still expose its live
state to an ancestor:
const outer = useForm<SignupValues>();
<FormContextProvider store={outer.store}> <SignupForm /></FormContextProvider>;<p>Valid: {String(outer.state.isFormValid)}</p>;State API
Section titled “State API”form.state is reactive. Access it inside Solid signals, createEffect, or JSX to get fine-grained
updates. Every name argument is a FieldPath<M>.
| Property or method | Type | Description |
|---|---|---|
isFormValid |
boolean |
true when no registered field has errors |
haveValuesChanged |
boolean |
true when any field has changed from its initial value |
isLoading |
boolean |
<Form isLoading> is set; every field is disabled |
isProcessing |
boolean |
An async submit handler is in flight, or <Form isProcessing> is set |
errors |
string[] |
Form-level errors, including thrown or rejected submit errors |
fields |
FormField[] |
Every registered field’s record: name, value, errors, and flags |
getField(name) |
FormField | undefined |
One field’s record |
getFieldValue(name) |
FieldPathValue<M, N> | undefined |
Current parsed value for a field |
getFieldErrors(name) |
string[] | undefined |
Current errors for a field |
isFieldValid(name) |
boolean | undefined |
false if the field has errors; undefined if not registered |
hasFieldBeenValid(name) |
boolean | undefined |
true once the field has been error-free |
hasFieldBlurred(name) |
boolean | undefined |
true once the user has blurred the field |
hasFieldChanged(name) |
boolean | undefined |
true once the field value has changed |
hasFieldBeenInitialized(name) |
boolean |
true once the field has registered with the store |
Mutations
Section titled “Mutations”The second element of form.store holds the store’s mutations:
const form = useForm<ProfileValues>();const [, mutations] = form.store;
mutations.reset(await api.loadProfile()); // load, then let the user editThe ones you are likely to call directly:
| Mutation | Description |
|---|---|
reset(toValues?) |
Revert every field to its initial value and clear form-level errors. With toValues, those values become the new initial values |
setValues(values) |
Set current values without changing the initial values or clearing errors |
resetField(name) |
Revert one field to its initial value and clear its errors |
setErrors(errors?) |
Replace the form-level errors, for example with a server response |
setFieldErrors(name, errors?) |
Replace one field’s errors |
setFieldsErrors(map) |
Set errors for many fields in one pass; registered fields missing from the map are cleared |
reset and setValues accept nested objects and match them to dotted field names:
setValues({ items: [{ title: 'x' }] }) sets the field registered as items.0.title. Keys with no
registered field are ignored. The remaining mutations exist for custom field components; see
Custom fields. See Loading and resetting data for
worked examples.
| Type | Description |
|---|---|
FieldPath<M> |
Every valid field name on M: top-level keys and dotted paths such as address.city or items.0.title |
FieldPathValue<M, P> |
The value type at path P |
FormState<M> |
The type of form.state |
FormStore<M> |
The type of form.store |
FormFieldProps |
Props for building your own field component; see Custom fields |
FieldPath follows objects and arrays up to six levels deep. A form typed with the default
FieldValueMapping, an untyped form, accepts any string.
Standard Schema Types
Section titled “Standard Schema Types”The package exports StandardSchemaV1, StandardSchemaV1Types, InferStandardSchemaInput,
InferStandardSchemaOutput, StandardSchemaFormValues, and StandardSchemaSubmitValues for custom
schema producers and helpers. StandardSchemaFormValues is the schema input type used by field
state; StandardSchemaSubmitValues is the schema output type passed to onSubmit.