SvForm
A schema-driven form that renders the SvGrid UI-kit controls from a FormField[],
with labels, required and custom validation, and a submit handler.
SvForm turns a declarative field list into a laid-out form - text, email, tel,
textarea, number, password, select, checkbox, switch, date, color, and rating
fields all map to the kit's editors. It validates on blur and on submit, emits
onSubmit(values) only when valid, and onChange(values) on every edit. Colors
come from the grid's --sg-* tokens, so it matches the rest of the kit.
Related: SvField · SvFileUpload · Layout & composite overview
Installation
Add it with the CLI - this drops a ready-to-edit SvForm starter into your app:
Prefer to see it first? npx @svgrid/ui try form opens it in a throwaway sandbox - no project needed.
Or install the package and import it directly. SvForm ships free in
@svgrid/grid (dependency-free):
import { SvForm } from '@svgrid/grid'
Example
Open the live example: Form (Layout)
<script lang="ts">
import { SvForm, type FormField } from '@svgrid/grid'
const fields: FormField[] = [
{ name: 'name', label: 'Full name', required: true },
{ name: 'email', label: 'Email', type: 'email', required: true },
{ name: 'plan', label: 'Plan', type: 'select',
options: [{ value: 'free', label: 'Free' }, { value: 'pro', label: 'Pro' }] },
{ name: 'agree', label: 'I accept the terms', type: 'checkbox' },
]
</script>
<SvForm {fields} columns={2} onSubmit={(values) => save(values)} />
Props
| Prop | Type | Default | Description |
|---|---|---|---|
fields |
ReadonlyArray<FormField | FormSection> |
- | The form schema - flat fields and/or titled sections. See FormField. |
initial |
Record<string, any> |
{} |
Initial values, seeded once on mount. |
onSubmit |
(values) => void | Promise<void> |
- | Fired with the (visible-field) values when valid. If it returns a promise, the submit button shows a loading state until it settles. |
onChange |
(values: Record<string, any>) => void |
- | Fired on every field edit. |
onCancel |
() => void |
- | When set, renders a secondary Cancel button. |
submitLabel |
string |
Submit |
Label for the submit button. |
cancelLabel |
string |
Cancel |
Label for the Cancel button. |
showReset |
boolean |
false |
Render a Reset button that restores the initial values. |
resetLabel |
string |
Reset |
Label for the Reset button. |
stepper |
boolean |
false |
Render titled sections as a validated multi-step wizard (Back / Next, per-step gating). |
tabs |
boolean |
false |
Render titled sections as tabs (one panel at a time; a failed submit jumps to the erroring tab). Ignored with stepper. |
columns |
number |
1 |
Columns in the responsive field grid (a section can override its own). |
disabled |
boolean |
false |
Disable every field and the submit button. |
serverErrors |
Record<string, string> |
- | Inject server-side errors ({ field: message }), applied reactively. |
formError |
string |
- | A form-level message shown in a banner above the fields. |
errorSummary |
boolean |
false |
Show a summary of field errors (with focus links) above the form after a failed submit. |
reinitialize |
off | always | ifPristine |
off |
Re-seed when the initial identity changes (e.g. editing a different record). ifPristine re-seeds only when there are no unsaved edits. |
dir |
ltr | rtl | auto |
- | Text direction; rtl mirrors the form layout. |
messages |
Partial<FormMessages> |
- | Override the built-in generated strings (required(label), minItems, maxItems, checking). |
FormField
type FormFieldType =
| 'text' | 'email' | 'tel' | 'textarea' | 'number' | 'password'
| 'select' | 'multiselect' | 'combobox' | 'checkbox' | 'switch' | 'radio'
| 'date' | 'datetime' | 'color' | 'rating' | 'slider' | 'tags'
| 'phone' | 'country' | 'mask' | 'file'
| 'array' // a repeatable group - see itemFields
type FormField = {
name: string
label: string
type?: FormFieldType // defaults to 'text'
required?: boolean
readonly?: boolean // shown but not editable (still submitted, unlike hidden)
help?: string // helper text under the control (distinct from an error)
placeholder?: string
options?: Array<{ value; label }> | ((values) => Array<{ value; label }>) // a function CASCADES (list derived from other values)
dependsOn?: string | string[] // clear this field when a parent changes (cascading selects)
mask?: string // for type: 'mask' (#=digit, A=letter, *=alnum)
min?: number; max?: number; step?: number; precision?: number // number / slider
prefix?: string; suffix?: string // number affixes ($ / %)
loadOptions?: (query: string) => Promise<Array<{ value; label }>> // remote combobox
accept?: string; multiple?: boolean // for type: 'file'
computed?: (values) => any // derive a read-only value from other fields (included in the payload)
rules?: ReadonlyArray<Validator> // declarative rules (email/pattern/min/compare...)
validate?: (value: any, values: Record<string, any>) => string | null | undefined
asyncValidate?: (value, values) => Promise<string | null | undefined> // debounced, stale-guarded
asyncDebounce?: number // ms before asyncValidate runs on edit (default 300)
full?: boolean // span the full width in the grid
span?: number // columns to span (generalizes full)
visible?: boolean | ((values) => boolean) // show only when true; hidden = not validated, not submitted
disabled?: boolean | ((values) => boolean) // disable, statically or derived from other values
// For type: 'array' (a repeatable group):
itemFields?: ReadonlyArray<FormField> // the fields of each row
addLabel?: string // "add" button label (default "+ Add")
minItems?: number // min / max row count
maxItems?: number
}
Beyond the basics, SvForm maps each rich type to the matching kit editor:
radio, slider, tags, phone, country, mask, datetime, combobox
(with remote loadOptions) and file. See the
rich field types demo.
Cascading fields: give a child a function options (derived from the current
values) and a dependsOn naming its parent(s). When the parent changes, the
child's list re-derives and its value is cleared so a stale selection never
lingers - for example Country -> State -> City:
{ name: 'country', label: 'Country', type: 'select', options: countries }
{ name: 'state', label: 'State', type: 'select', dependsOn: 'country',
options: (v) => statesByCountry[v.country] ?? [] }
Computed fields: computed: (values) => ... derives a read-only value from
the other fields; it recomputes reactively, is never user-validated, and its
value is included in the submitted payload - a live total, for instance:
{ name: 'qty', label: 'Qty', type: 'number' }
{ name: 'price', label: 'Unit price', type: 'number' }
{ name: 'total', label: 'Total', computed: (v) => (v.qty ?? 0) * (v.price ?? 0) }
FormSection
Group fields under a heading. A section renders as a titled fieldset, and with
<SvForm stepper> each section becomes one validated step of a wizard. The schema
may freely mix flat fields and sections.
type FormSection = {
section: string // heading (and step label)
description?: string
fields: ReadonlyArray<FormField>
columns?: number // this section's grid columns (defaults to the form's)
}
Examples
Conditional (dynamic) fields
visible and disabled take a value or a (values) => boolean, so a field can
appear, hide or disable based on the rest of the form. A hidden field is
skipped in validation and left out of the submitted payload - so a conditionally
required field never blocks submit while it's hidden.
Open the live example: Dynamic form (Layout)
const fields: FormField[] = [
{ name: 'contact', label: 'Preferred contact', type: 'select', required: true, options: [
{ value: 'email', label: 'Email' }, { value: 'phone', label: 'Phone' }, { value: 'none', label: 'Do not contact me' },
] },
{ name: 'email', label: 'Email address', type: 'email', required: true,
rules: [rules.email()], visible: (v) => v.contact === 'email' },
{ name: 'notes', label: 'Notes', type: 'textarea',
disabled: (v) => v.contact === 'none' },
]
Sections and wizard steps
Group fields into FormSections and they render as titled fieldsets. Add stepper
and each section becomes a validated step - Next only advances when the current
step is valid, and the last step submits. Or add tabs to render the sections as
tabs instead (one panel at a time; a failed submit jumps to the first tab that has
an error, which is also marked):
Open the live example: Wizard form (Layout)
const schema: FormEntry[] = [
{ section: 'Account', fields: [
{ name: 'email', label: 'Email', type: 'email', required: true, rules: [rules.email()] },
] },
{ section: 'Profile', columns: 2, fields: [
{ name: 'first', label: 'First name', required: true },
{ name: 'last', label: 'Last name', required: true },
] },
]
<SvForm fields={schema} stepper columns={2} onSubmit={save} />
Field arrays (repeatable groups)
A type: 'array' field with itemFields renders add / remove rows (and up / down
reorder buttons once there is more than one row). Its value is an array of item
objects; each row validates against itself (so within-row rules work), and
required / minItems / maxItems gate the row count:
const schema: FormEntry[] = [
{ name: 'items', label: 'Line items', type: 'array', required: true, minItems: 1, itemFields: [
{ name: 'desc', label: 'Description', required: true },
{ name: 'qty', label: 'Qty', type: 'number', required: true, rules: [rules.min(1)] },
] },
]
// value: { items: [{ desc: 'Widget', qty: 3 }, ...] }
Driving createForm directly, arrays expose arrayItems(name), itemValue,
itemError, addItem(name, item?), removeItem(name, i), moveItem,
setItemValue, and handleItemBlur.
Async validation
asyncValidate runs after the sync checks pass - debounced (asyncDebounce,
default 300ms) and stale-guarded so only the latest response wins. While it runs
the field shows a checking indicator (form.isValidating(name)), and submit waits
for every async validator before it fires:
{ name: 'username', label: 'Username', required: true,
asyncValidate: async (v) => (await isTaken(v)) ? 'That username is taken' : null }
Async submit, reset, and server errors
onSubmit may return a promise - the submit button shows a loading state until it
settles. Set showReset for a Reset button that restores the initial values. For
server-side validation, drive the form from the headless createForm core and
call setErrors after the request:
<script lang="ts">
import { createForm } from '@svgrid/grid'
const form = createForm({ fields: () => fields, initial, onSubmit: save })
async function submit() {
if (!(await form.submit())) return
const res = await api.save(form.values)
if (!res.ok) form.setErrors(res.fieldErrors) // { email: 'Already taken' }
}
</script>
createForm also exposes submitting, isDirty, isFieldDirty(name),
isVisible(name), isDisabled(name), and reset(next?).
Two-column layout with full-width rows
Set columns={2} and mark wide fields full so they span both columns:
const fields: FormField[] = [
{ name: 'first', label: 'First name', required: true },
{ name: 'last', label: 'Last name', required: true },
{ name: 'bio', label: 'Bio', type: 'textarea', full: true },
]
Cross-field validation
validate receives the whole values object, so one field can check another - for
example confirming a password:
{ name: 'confirm', label: 'Confirm password', type: 'password', required: true,
validate: (v, values) => (v === values.password ? null : 'Passwords do not match') }
Declarative rules
Use rules for reusable checks (email, pattern, min, compare) instead of hand
writing validate. Required, rules, then validate run in that order, and the
first failure wins.
Sign-up form with declarative rules
Compose rules per field rather than hand-writing validate. compare reads
another field for a cross-field check, so it confirms the password inline:
<script lang="ts">
import { SvForm, rules, type FormField } from '@svgrid/grid'
const fields: FormField[] = [
{ name: 'email', label: 'Email', type: 'email', required: true, rules: [rules.email()] },
{ name: 'password', label: 'Password', type: 'password', required: true,
rules: [rules.minLength(8)] },
{ name: 'confirm', label: 'Confirm password', type: 'password', required: true,
rules: [rules.compare('password', '===', { message: 'Passwords do not match' })] },
{ name: 'age', label: 'Age', type: 'number', rules: [rules.min(18)] },
]
</script>
<SvForm {fields} columns={2} submitLabel="Create account"
onSubmit={(values) => register(values)} />
Tip: every rule builder except required skips empty values, so rules.min(18)
only fires once the optional age field is filled - add required when the field
must also be present.
Accessibility
- Every field renders a
<label>wired to its control; required fields add a visible marker and validation blocks submit. - Errors render with
role="alert"so they are announced when they appear, and each control is wired to its message witharia-invalid+aria-describedby. - The form sets
novalidateand runs its own validation, so messages are consistent across browsers. dir="rtl"mirrors the layout, and every generated string is overridable viamessagesfor localization.
More examples
Form: rich field types
SvForm reaching the whole input suite from one schema - phone, country, mask, combobox, radio, slider, tags, datetime and file - plus per-field help text, a readonly field, column span, and a promise toast on submit.
Form: cascading fields
SvForm dependent selects - a child list derives from the parent value via a function options, and dependsOn clears the child when the parent changes so a stale selection never lingers. Country -> State -> City.
Checkout form
A real payment form built entirely from SvGrid UI: SvMaskedInput (card number + expiry + CVC), SvCountryInput (billing country), SvNumberInput (amount), SvSwitchButton (save card) and SvButton - with live card-brand detection and validation. Copy the file and ship it.
Open the live example: Checkout form (Recipes)
Appointment booking
A scheduling form from SvGrid UI: SvComboBox (service), SvCalendar (date), SvTimePicker (slot), SvButtonGroup (duration), SvNumberInput (guests) and a live summary. The same components SvGrid uses to edit cells, composed into a page.
See also
- SvField - the shared label / hint / error wrapper for building custom form rows.
- SvFileUpload - a file field that carries the same editor contract.
- Layout overview - the whole layout family at a glance.
Live examples
- Form - SvForm: a schema-driven signup form wiring the whole kit (text, email, password, select, date, switch, rating) with required + cross-field custom validation in a two-column grid.
- Dynamic form - SvForm with conditional fields: visible / disabled derived from other values (hidden fields skip validation and drop from the payload), async field validation, an async onSubmit with a loading button, and a Reset that restores the initial values.
- Wizard form - SvForm as a multi-step wizard: titled FormSection groups become validated steps with Back / Next (gated on each step) and a final async Submit. Drop the stepper prop and the same schema renders as stacked fieldsets.
- Field array form - SvForm field array (type: array): a repeatable group with add / remove rows, per-row validation (each item validates against itself), required / minItems gating, and an array value in the submitted payload - an invoice line-items form.
- Form: rich field types - SvForm reaching the whole input suite from one schema - phone, country, mask, combobox, radio, slider, tags, datetime and file - plus per-field help text, a readonly field, column span, and a promise toast on submit.
- Form: cascading fields - SvForm dependent selects - a child list derives from the parent value via a function `options`, and `dependsOn` clears the child when the parent changes so a stale selection never lingers. Country -> State -> City.
- Checkout form - A real payment form built entirely from SvGrid UI: SvMaskedInput (card number + expiry + CVC), SvCountryInput (billing country), SvNumberInput (amount), SvSwitchButton (save card) and SvButton - with live card-brand detection and validation. Copy the file and ship it.
- Appointment booking - A scheduling form from SvGrid UI: SvComboBox (service), SvCalendar (date), SvTimePicker (slot), SvButtonGroup (duration), SvNumberInput (guests) and a live summary. The same components SvGrid uses to edit cells, composed into a page.
Related articles
- SvGrid UI Components Tips and Tricks: Svelte 5 Buttons, Inputs, Tree, Forms - Practical tips for the SvGrid UI component kit - SvButton states, a Cmd+K command palette, a virtualized searchable tree, schema-driven forms, a parsing date/time field, and the shared field contract - each with a code snippet.