Skip to content

API reference

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

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).

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} />

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.

Same props as InputField. Renders <input type="password">.

Same props as InputField, except type. Renders a <textarea>.

<TextAreaField name='bio' label='Bio' maxLength={500} />

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

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.

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.

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} />

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' />

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 />

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"

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.

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.

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.

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>;

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

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 edit

The 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.

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.