@pyreon/form — API Reference
Generated from
form'ssrc/manifest.ts— the same source that powersllms.txtand MCPget_api. Do not edit this page by hand; edit the manifest. For the conceptual guide, see form.
Signal-based form management for Pyreon. Each field (value, error, touched, dirty) is its own Signal<T> so templates only re-run for the slice they read. First-class schema validation — pass a RAW Standard Schema (zod / valibot / arktype / @pyreon/validate) straight to schema with no adapter and no cast, or a zodSchema / valibotSchema / arktypeSchema typed adapter from @pyreon/validation. Dot-path field keys ("address.city") declare first-class LEAF fields whose per-field-validator / schema errors route to the exact leaf — a REAL nested declarative schema (z.object({ address: z.object({ city }) })) is fed the nested value shape and its per-leaf-path errors auto-split to the leaf fields; a nested error with no leaf field routes to the nearest ancestor object field, and a key matching no field invalidates rather than being silently dropped. Per-field validateOn: blur | change | submit, async validators with optional debounceMs and version-based stale-result discarding, cross-field validation via (value, allValues) => …, type-aware input bindings (register({ type: "checkbox" | "number" | "file" })), runtime field registration (registerField / unregisterField) for data-driven forms, focus-first-error on failed submit (focusOnError, default on), reset ergonomics (reset(values, { keep* }) + resetField(field, { keepError })), dynamic useFieldArray with stable keys for keyed rendering, and typed useWatch overloads for single / multi / all-field reactive watchers.
Features
Per-field fine-grained signals (value, error, touched, dirty independent)
useForm / useField / useFieldArray / useWatch / useFormState
FormProvider + useFormContext — no prop drilling
Per-field and schema-level validation (Zod / Valibot / ArkType via
@pyreon/validation)validateOn: 'blur' | 'change' | 'submit' — default 'blur'
Async validators with optional debounceMs + version-based stale-result discard
Cross-field validation — validator receives
(value, allValues)useFieldArray: append / prepend / insert / remove / update / move / swap / replace with stable keys
Server-side errors: setFieldError / setErrors / clearErrors
register({ type: "checkbox" | "number" | "file" })— type-aware input bindings (file writes the FileList)Raw Standard Schema in
schema(zod / valibot / arktype /@pyreon/validate) — no adapter, no castDynamic runtime fields —
registerField/unregisterFieldfor data-driven formsFocus-first-error on failed submit (
focusOnError, default on) +focusFirstError()Reset ergonomics —
reset(values, { keep* })new baseline +resetField(field, { keepError })Dot-path leaf fields —
"address.city"is a first-class field; errors route to the leaf (nestValues/flattenValuesfor nested payloads)Schema-error routing — leaf/ancestor field; orphan key invalidates (no silent drop)
Complete example
A full, end-to-end usage of the package:
import { useForm, useField, useFieldArray, useWatch, useFormState, FormProvider } from '@pyreon/form'
import { zodSchema } from '@pyreon/validation/zod'
import { z } from 'zod'
// 1. useForm — entry point. initialValues is the single source of truth
// for field keys + types. onSubmit receives validated values.
const form = useForm({
initialValues: { email: '', password: '', remember: false, tags: [] as string[] },
validators: {
email: (v) => (!v ? 'Required' : undefined),
// Cross-field: validator receives (value, allValues)
password: (v, all) =>
v.length < 8 ? 'Too short' : v === all.email ? 'Password must differ from email' : undefined,
},
schema: zodSchema(z.object({
email: z.string().email(),
password: z.string().min(8),
})),
validateOn: 'blur', // 'blur' (default) | 'change' | 'submit'
debounceMs: 300, // optional — stale async results are discarded via version counter
onSubmit: async (values) => { await api.login(values) },
})
// 2. register() — bind an input. Returns { value, onInput, onBlur } and,
// for type: 'checkbox', also a `checked` accessor.
<form onSubmit={form.handleSubmit}>
<input {...form.register('email')} />
<input type="password" {...form.register('password')} />
<input type="checkbox" {...form.register('remember', { type: 'checkbox' })} />
</form>
// 3. useField — extract one field's state for isolated components.
// `hasError` / `showError` are computeds so you don't recompute
// the touched-AND-error condition at every call site.
function EmailField({ form }: { form: typeof form }) {
const field = useField(form, 'email')
return (
<>
<input {...field.register()} />
{() => field.showError() && <span class="error">{field.error()}</span>}
</>
)
}
// 4. useFieldArray — dynamic arrays with stable keys for <For>.
const tags = useFieldArray<string>(['typescript'])
tags.append('pyreon')
tags.prepend('signals')
tags.insert(1, 'reactive')
tags.move(0, 2)
tags.swap(0, 1)
tags.remove(0)
// tags.items() → FieldArrayItem<string>[] — { key, value: Signal<T> }
// Use the stable key in <For by={i => i.key}> so re-renders don't thrash.
// 5. useWatch — typed overloads: single field → Signal<T>,
// multiple fields → [Signal<A>, Signal<B>], no args → Computed<TValues>.
const email = useWatch(form, 'email') // Signal<string>
const [first, last] = useWatch(form, ['firstName', 'lastName'])
const everything = useWatch(form) // Computed<TValues>
// 6. useFormState — derived form-level summary. Pass a selector to avoid
// re-rendering when unrelated fields move.
const canSubmit = useFormState(form, (s) => s.isValid && !s.isSubmitting && s.isDirty)
// 7. FormProvider / useFormContext — skip prop-drilling across deep trees.
<FormProvider form={form}>
<DeepInputs />
</FormProvider>
// Inside DeepInputs: const form = useFormContext<MyValues>()
// 8. Server errors — setFieldError / setErrors / clearErrors after a
// failed submit. Does NOT touch touched state, so the error shows
// regardless of blur status.
form.setErrors({ email: 'Already registered' })
// 9. Raw Standard Schema — pass zod/valibot/arktype straight to `schema`,
// no zodSchema() adapter and no `as never` cast.
import { z } from 'zod'
const typed = useForm({
initialValues: { email: '', age: 0 },
schema: z.object({ email: z.string().email(), age: z.number().min(13) }),
onSubmit: async (v) => { await api.save(v) },
})
// 10. File input — a value-less bag; onInput writes the FileList.
// field.value() is `FileList | null`; read `files?.[0]` for one file.
<input type="file" {...form.register('avatar', { type: 'file' })} />
// 11. Dynamic fields — add/remove first-class fields at runtime.
form.registerField('phone', '', (v) => (v ? undefined : 'Required'))
form.setFieldValue('phone', '555-0100') // reaches values()/onSubmit/validity
form.unregisterField('phone') // cleanly drops its invalid/dirty contribution
// 12. Reset to freshly-saved server data + keep-options.
form.reset(await api.save(form.values())) // named fields = new baseline; isDirty() → false
form.reset(undefined, { keepErrors: true }) // also keepTouched / keepDirty / keepSubmitCount
// 13. Focus-first-error — on by default on a failed submit; also manual.
if (!(await form.validate())) form.focusFirstError()Exports
| Symbol | Kind | Summary |
|---|---|---|
useForm | hook | Create a signal-based form. |
useField | hook | Extract a single field's state and helpers from a form instance — avoids passing the entire FormState to leaf componen |
useFieldArray | hook | Manage a dynamic array of form fields with stable keys. |
useWatch | hook | Typed overloads for reactively watching form field values. |
useFormState | hook | Computed summary of form-level state (isValid, isDirty, isSubmitting, isValidating, submitCount, errors). |
FormProvider | component | Provide a form via context so nested components can read it with useFormContext<TValues>() without prop-drilling. |
useFormContext | hook | Read the nearest FormProvider form from context. |
FormValues | type | Derive the TValues shape from a form — accepts BOTH the useForm RETURN (FormState<V>) and the useForm OPTIONS (` |
FieldNames | type | The field-name union of a form. |
FieldValue | type | The value type of ONE field of a form, by field name — FieldValue<typeof form, "age"> is number. |
NestValues | type | Type-level companion of the runtime nestValues(): convert a FLAT dot-path value shape ({ 'address.city': string }) t |
API
useForm hook
<TValues extends Record<string, unknown>>(options: UseFormOptions<TValues>) => FormState<TValues>Create a signal-based form. initialValues drives field keys and types end-to-end — TValues is inferred from it, so all downstream typings (useField field name, useWatch keys, validator signatures) are fully typed without annotation. Returns FormState<TValues> with per-field signals, form-level signals (isSubmitting, isValidating, isValid, isDirty, submitCount, isSubmitted, isSubmitSuccessful, submitError), and handlers (handleSubmit, reset, validate, trigger, focusFirstError, registerField, unregisterField). react-hook-form-parity accessors: trigger(name?) validates a field/subset/whole-form on demand; getValues(name?) reads one value or all; dirtyFields() / touchedFields() return the changed/visited fields as records; getFieldState(name) returns a field's live signals (undefined for a name matching no field — an existence probe for dynamic fields); isSubmitted / isSubmitSuccessful track submit lifecycle. validateOn defaults to "blur" (not "change") so users aren't scolded mid-keystroke. schema accepts a RAW Standard Schema (zod / valibot / arktype / @pyreon/validate's s) directly — no adapter, no as never cast — as well as a plain SchemaValidateFn or a @pyreon/validation typed adapter (zodSchema / valibotSchema / arktypeSchema); it runs after per-field validators, field-level errors win on the same field, and a key matching no field invalidates the form (never silently dropped). A field key with a dot ("address.city") declares a FIRST-CLASS leaf field: per-field validators route to the exact leaf, and a DECLARATIVE schema (raw Standard Schema or typed adapter — never a plain function) over a dot-path-leaf form is fed the NESTED value shape (rebuilt from the flat model) so a real nested z.object({ address: z.object({ city }) }) validates correctly and its address.city error auto-splits to the leaf field; a nested error with no leaf field routes to the nearest ancestor object field. The value model is FLAT (values() / onSubmit keep the dot-path keys — honest field-name types); nestValues / flattenValues convert to/from a nested API payload. an UNRECOGNIZED schema object (e.g. a zod<3.24 schema with no ~standard support, or a mistyped object) THROWS at creation instead of silently disabling validation. register(field, { type }) binds an input per type — "checkbox" → checked, "number" → valueAsNumber, "file" → a value-less bag whose onInput writes the FileList. registerField(name, initial?, validator?) / unregisterField(name) add or remove first-class fields at runtime for data-driven forms (idempotent; no silent auto-registration; dynamic fields are runtime-typed). On a failed submit, focus moves to the first errored + register()-bound field unless focusOnError: false (also exposed as focusFirstError()). reset(values?, { keepErrors?, keepTouched?, keepDirty?, keepSubmitCount? }) resets to a new baseline — DURABLY: named fields become the new baseline for the dirty compare, resetField, and any later plain reset() (react-hook-form defaultValues-replacement parity); unnamed fields revert to their current baseline. resetField(field, { keepError?, keepTouched? }) resets one field. An explicit null/undefined field value is first-class — values() and the submit payload never fall back to the initial for it (a cleared FileList | null file field submits null).
Example
const form = useForm({
initialValues: { email: '', password: '' },
validators: {
email: (v) => (!v ? 'Required' : undefined),
password: (v, all) => (v.length < 8 ? 'Too short' : undefined),
},
onSubmit: async (values) => { await login(values) },
})
// Bind inputs with register():
// h('input', form.register('email'))
// h('input', { type: 'checkbox', ...form.register('remember', { type: 'checkbox' }) })Common mistakes
Mutating
initialValuesafter creation — it is read once at setup; usesetFieldValuefor programmatic updatesReading
form.fields[name].valueas a plain value — it isSignal<T>, call it:form.fields.email.value()Passing
validateOn: "change"withoutdebounceMson async validators — fires a network request on every keystrokeCalling
form.handleSubmit()without attaching it as a formonSubmithandler — it callspreventDefault()so it must receive the form event, or be called with no argument for programmatic submitAssuming
schemaerrors override field validators — it is the reverse:schemaruns AFTER per-fieldvalidatorsbut a field that already has a field-level error KEEPS it; the schema only fills fields with no field-level error. Also: a raw Standard Schema needs NOzodSchema()wrapper oras nevercast — pass zod/valibot/arktype directlyPassing a pre-Standard-Schema library object as
schema(zod<3.24, or a mistyped object) — useForm THROWS at creation with[Pyreon]guidance; it never silently skips validation. Upgrade the library or pass a@pyreon/validationadapterExpecting
values()/onSubmitto be NESTED for dot-path fields — the value model is FLAT: a"address.city"field surfaces asvalues()["address.city"], NOTvalues().address.city. Convert at the backend boundary withnestValues(form.values()). (A NESTED declarative schema likez.object({ address: z.object({…}) })DOES work — itsaddress.cityerror auto-splits to the leaf field — but until typed deep-path inference lands it needs anas nevercast, sinceschemais typed against the flat keys)
See also: useField · FormProvider · useFormState
useField hook
<TValues, K extends keyof TValues & string>(form: FormState<TValues>, name: K) => UseFieldResult<TValues[K]>Extract a single field's state and helpers from a form instance — avoids passing the entire FormState to leaf components. Returns all FieldState signals (value, error, touched, dirty) plus two convenience computeds: hasError (true when an error string exists) and showError (true when touched AND errored — the typical UI condition for gating error display). Also exposes register(opts?) to bind an <input> element with a single spread.
Example
function EmailField({ form }: { form: FormState<{ email: string }> }) {
const field = useField(form, 'email')
return (
<>
<input {...field.register()} />
{() => field.showError() && <span>{field.error()}</span>}
</>
)
}Common mistakes
Destructuring
const { value } = useField(form, "email")and callingvalue()— works, but the getter evaluates to the Signal itself; storingvalue()at setup captures the initial value and defeats reactivityForgetting
showErrorand reimplementingtouched() && hasError()in every template —showErroris aComputed<boolean>, use it directly
See also: useForm · useWatch
useFieldArray hook
<T>(initial?: T[]) => UseFieldArrayResult<T>Manage a dynamic array of form fields with stable keys. Each item is { key: number, value: Signal<T> } — use item.key inside <For by={i => i.key}> so reordering / inserts do not remount child components. Full mutation surface: append, prepend, insert, remove, update, move, swap, replace.
Example
const tags = useFieldArray<string>([])
tags.append('typescript')
tags.prepend('signals')
tags.insert(1, 'reactive')
tags.move(0, 2)
tags.remove(0)
// Keyed rendering — never drop the `by={i => i.key}`
<For each={tags.items()} by={(i) => i.key}>
{(item) => <input value={item.value()} onInput={(e) => item.value.set(e.currentTarget.value)} />}
</For>Common mistakes
Rendering with <For by={(_, i) => i}> — index-based keys lose identity on reorder, defeating the stable-key design
Calling tags.items() inside setup and storing the array — it is a Signal, read inside reactive scopes
See also: useForm
useWatch hook
(form, name) => Signal<TValues[K]> | (form, names[]) => Signal<T>[] | (form) => Computed<TValues>Typed overloads for reactively watching form field values. Single-field form returns Signal<T> (fast path — same signal, no wrapper), multi-field returns a tuple of signals, no-args returns a Computed<TValues> over the whole values object. Prefer the narrowest form — watching everything re-runs your effect when ANY field changes.
Example
const email = useWatch(form, 'email') // Signal<string>
const [first, last] = useWatch(form, ['firstName', 'lastName'])
const everything = useWatch(form) // Computed<TValues>
// Derive and sync: preview displays the email as the user types.
effect(() => { preview.set(`Hello ${email()}`) })Common mistakes
Using the all-fields overload (
useWatch(form)) to derive a single computed — re-runs when any field changes, not just the one you care about. UseuseWatch(form, "email")for single-field precision
See also: useFormState · useField
useFormState hook
<TValues, T>(form: FormState<TValues>, selector?: (s: FormStateSummary) => T) => Computed<T>Computed summary of form-level state (isValid, isDirty, isSubmitting, isValidating, submitCount, errors). Passing a selector restricts the tracked subset — a button driven by canSubmit should not re-render just because submitCount changed. Without a selector, the computed re-derives on ANY form-level state change.
Example
const canSubmit = useFormState(form, (s) => s.isValid && !s.isSubmitting && s.isDirty)
<button disabled={!canSubmit()}>Save</button>Common mistakes
Omitting the selector and reading
useFormState(form)as a whole — triggers on every field change, every validation, every submit count bump. Always pass a selector for UI-bound computeds
See also: useForm · useWatch
FormProvider component
<TValues>(props: { form: FormState<TValues>; children: VNodeChild }) => VNodeProvide a form via context so nested components can read it with useFormContext<TValues>() without prop-drilling. Every call to useFormContext inside the provider tree returns the same FormState instance. Nest inside PyreonUI or any other provider — the form context is independent.
Example
<FormProvider form={form}>
<PersonalInfoSection />
<AddressSection />
<SubmitButton />
</FormProvider>
// Inside any descendant:
const form = useFormContext<typeof values>()Common mistakes
Nesting
FormProviderwithin itself expecting scoped forms — the inner provider shadows the outer; for multi-form pages, use separate providers at sibling level, not nested
See also: useFormContext · useForm
useFormContext hook
<TValues>() => FormState<TValues>Read the nearest FormProvider form from context. Throws at dev time if no provider is mounted above the call site. Pass the expected TValues generic so downstream typings (useField field names, useWatch keys) stay end-to-end typed. Returns the same FormState<TValues> instance that was passed to FormProvider.
Example
const form = useFormContext<{ email: string; password: string }>()
const field = useField(form, 'email')Common mistakes
Calling at module scope — hooks require an active component setup context; call inside a component body
Omitting the
<TValues>generic — TypeScript infersFormState<Record<string, unknown>>anduseFieldfield names lose type narrowing
See also: FormProvider · useForm
FormValues type
type FormValues<F> // FormState<V> | UseFormOptions<V> → VDerive the TValues shape from a form — accepts BOTH the useForm RETURN (FormState<V>) and the useForm OPTIONS (UseFormOptions<V>), so a generic wrapper can derive the value shape from whichever it holds instead of threading a second type parameter. Type-only, zero runtime bytes.
Example
const form = useForm({ initialValues: { email: '', age: 0 }, onSubmit: () => {} })
type Values = FormValues<typeof form> // { email: string; age: number }Common mistakes
Passing a plain object type —
FormValues<{ email: string }>isnever; the input is the FORM (or its options), not the values themselvesFields added at runtime via
registerField()are not in the static shape — read those viagetValues()[name]
See also: FieldNames · FieldValue · useForm
FieldNames type
type FieldNames<F> = keyof FormValues<F> & stringThe field-name union of a form. Dot-path leaf fields keep their FLAT keys ('address.city' stays ONE field name — the form's value model is flat by design). Type-only, zero runtime bytes.
Example
type Names = FieldNames<typeof form> // 'email' | 'age'
function focusField(name: FieldNames<typeof form>) { /* … */ }Common mistakes
Expecting 'address.city' to split into nested names — dot-path leaves are first-class FLAT field names in @pyreon/form
See also: FormValues · FieldValue
FieldValue type
type FieldValue<F, K extends FieldNames<F>>The value type of ONE field of a form, by field name — FieldValue<typeof form, "age"> is number. The key is constrained to the real field names, so a typo is a compile error. Type-only, zero runtime bytes.
Example
type Age = FieldValue<typeof form, 'age'> // numberCommon mistakes
A mistyped field name fails typecheck by design — that is the feature, not a bug to cast around
See also: FormValues · FieldNames
NestValues type
type NestValues<T extends Record<string, unknown>> // flat dot-path shape → nested payload shapeType-level companion of the runtime nestValues(): convert a FLAT dot-path value shape ({ 'address.city': string }) to its NESTED payload shape ({ address: { city: string } }). STANDALONE and opt-in by design — useForm/values()/onSubmit deliberately keep the FLAT keys (threading a nested shape through the form signature breaks generic wrappers like @pyreon/feature); use this to type YOUR OWN API boundary. Recursion follows the dot count (realistic keys ≤ 6 segments are fine). Type-only, zero runtime bytes.
Example
const form2 = useForm({
initialValues: { name: '', 'address.city': '', 'address.zip': '' },
onSubmit: (values) => {
const payload = nestValues(values) as NestValues<typeof values>
// payload: { name: string; address: { city: string; zip: string } }
},
})Common mistakes
Expecting
useFormitself to expose nested values — the value model is FLAT end-to-end;NestValuestypes thenestValues()boundary you ownNumeric segments (
"tags.0") type as an indexed OBJECT while the runtime builds a real ARRAY — cast at the boundary if you rely on array methodsDeclaring both an object field (
address) AND a leaf ("address.city") — the type unions at theaddresskey and the form dev-warns; pick one shapeApplying it to already-nested values — it is flat-in/nested-out; a keyless-dot shape passes through unchanged
See also: FormValues · useForm
Package-level notes
validateOn default is
blur, notchange: Fields validate on blur by default so users aren't scolded mid-keystroke. UsevalidateOn: "change"for instant feedback (often paired withdebounceMs: 300to avoid thrashing async validators), orvalidateOn: "submit"for zero-feedback-until-submit forms.showError(fromuseField) gates ontouched, so even withvalidateOn: "change"errors won't appear until the user has blurred at least once — this is intentional.
Field signals are independent: Every
FieldState<T>field (value,error,touched,dirty) is its ownSignal<T>— readingfield.value()does not subscribe tofield.error(). Pair withuseFieldsohasError/showErrorare computed once per field instead of recomputed at every call site.
Stable keys in
useFieldArray:FieldArrayItem<T>.keyis a monotonically increasing number assigned at insert time — NOT the array index. Use<For each={items()} by={(item) => item.key}>so move / insert / remove preserve component identity and input focus. Index-based keys defeat the stable-key design and cause children to remount on every reorder.
Async validators + stale results: Async validators are version-tracked: if the user types faster than the validator resolves, the stale result is discarded when it finally returns. Combine with
debounceMsto also cut down the number of in-flight requests. TheisValidatingsignal is true while any field has a pending async validation — use it to gate the submit button.
Server errors via
setFieldError/setErrors: After a failed submit, attach server-side errors withform.setFieldError(name, msg)orform.setErrors({ email: "Taken" }). These do NOT touchtouchedstate, so errors display immediately regardless of blur status.clearErrors()wipes them on the next keystroke ifvalidateOn: "change"is set, or on next submit otherwise.
Raw Standard Schema — no adapter, no cast:
schemaaccepts a raw Standard Schema (zod ≥ 3.24 / valibot ≥ 1 / arktype ≥ 2 /@pyreon/validate'ss) DIRECTLY — passz.object({...})with nozodSchema()wrapper and noas nevercast. It also still accepts a plainSchemaValidateFnand a@pyreon/validationtyped adapter (reach for the adapter when you want the schema's field names checked againstTValuesat compile time). The~standardcontract + bridge live in@pyreon/validation; form re-exportsValidationError/ValidateFn/SchemaValidateFnso the historicalimport { ValidationError } from "@pyreon/form"keeps working.
Dot-path leaf fields + nested schema-error routing: A field key containing a dot (
"address.city") declares a FIRST-CLASS leaf field, addressable exactly like a top-level one —register("address.city"),useField("address.city"),setFieldValue,validators: { "address.city": … }, and a schema/validator error keyed"address.city"route to it. The value model is FLAT:values()/getValues()/onSubmitreturn the flat keys ({ "address.city": … }) so field-name types stay honest; convert to a nested API payload withnestValues(form.values())and back withflattenValues(serverData). Two error-routing modes: (1) per-field validators or a FLAT-keyed schema (s.object({ "address.city": … })) route to the exact LEAF field; (2) a NESTED schema whose value is a single OBJECT field ({ address: { city } }→ fieldaddress) routes its dot-path error to that ANCESTOR object field. A schema-error key matching NO field (a shape mismatch, or the path-less""whole-form key) marks the form invalid, setssubmitError, and dev-warns — never silently dropped. Declaring BOTH an object fieldaddressAND a leafaddress.citydev-warns (the error would show on both). Field-level errors win over schema errors on the same field.
File inputs are value-less:
register(field, { type: "file" })returns a bag WITHOUTvalueorchecked— a file input can't be value-controlled (<input type="file" value=…>is rejected). ItsonInputwrites the input'sFileList(target.files) to the field, sofield.value()isFileList | null— readfiles?.[0]for a single file, or iterate for amultipleinput. The value flows intovalues()/onSubmitlike any field.
Dynamic fields need explicit
registerField— no auto-registration:@pyreon/formnever lazily registers a field on firstsetFieldValue/register— that would silently drop data. Add a runtime field withform.registerField(name, initial?, validator?)(idempotent: re-registering keeps the current value, refreshes the validator) and remove it withform.unregisterField(name)(cleanly zeroes itsisValid()/isDirty()contribution). Dynamic fields are runtime-typed — not part of the staticTValues— so read them viagetValues()[name]/fields[name].
Focus-first-error is on by default: On a failed
handleSubmit, focus moves to the first errored field (declaration order) that was bound viaregister()— accessible error recovery. Opt out withfocusOnError: false. A field never passed throughregister()has no known id and is skipped.form.focusFirstError()is exposed for custom submit flows; both are SSR-safe no-ops on the server.