Form
Centralized form validation and submission. Supports schema validation (Zod, Valibot, Yup, Joi), custom validators, async submit, nested forms, programmatic API, and more.
validateOn: 'focus' now waits for the first blur before showing an error, which is the rule 'input' already followed. eagerValidation on a FormField opts a field back into checking from the very first focus. Defaults and submit behaviour
are unchanged.Basic Usage
A simple login form with Zod schema validation. Errors appear automatically on each FormField that matches by name.
<script lang="ts">
import { Form, FormField, Input, Button, toast } from 'sv5ui';
import type { FormApi } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
email: z.email('Invalid email address'),
password: z.string().min(6, 'Password must be at least 6 characters')
});
let state = $state({});
let api = $state<FormApi>();
</script>
<Form {schema} bind:state bind:api onsubmit={() => { toast.success('Form submitted successfully!'); }} class="space-y-4">
<FormField label="Email" name="email" required>
<Input bind:value={state.email} placeholder="you@example.com" leadingIcon="lucide:mail" />
</FormField>
<FormField label="Password" name="password" required>
<Input bind:value={state.password} type="password" placeholder="Enter password" leadingIcon="lucide:lock" />
</FormField>
<Button type="submit" label="Sign In" loading={api?.loading} block />
</Form>Schema Libraries
Form supports Zod, Valibot, Yup (via Standard Schema), and Joi out of the box. Switch between tabs to see each library in action with the same form.
<script lang="ts">
// --- Zod (v3.24+ / v4) ---
import { z } from 'zod';
const zodSchema = z.object({
email: z.email('Invalid email'),
password: z.string().min(8, 'At least 8 characters')
});
// --- Valibot (v1.0+) ---
import * as v from 'valibot';
const valibotSchema = v.object({
email: v.pipe(v.string(), v.email('Invalid email')),
password: v.pipe(v.string(), v.minLength(8, 'At least 8 characters'))
});
// --- Yup (v1.7+) ---
import * as yup from 'yup';
const yupSchema = yup.object({
email: yup.string().email('Invalid email').required('Email is required'),
password: yup.string().min(8, 'At least 8 characters').required('Password is required')
});
// --- Joi (v17+) ---
import Joi from 'joi';
const joiSchema = Joi.object({
email: Joi.string().email({ tlds: false }).required()
.messages({ 'string.email': 'Invalid email', 'string.empty': 'Email is required' }),
password: Joi.string().min(8).required()
.messages({ 'string.min': 'At least 8 characters', 'string.empty': 'Password is required' })
});
</script>
<!-- All four work identically with the Form component -->
<Form schema={zodSchema} bind:state onsubmit={handler} class="space-y-4">...</Form>
<Form schema={valibotSchema} bind:state onsubmit={handler} class="space-y-4">...</Form>
<Form schema={yupSchema} bind:state onsubmit={handler} class="space-y-4">...</Form>
<Form schema={joiSchema} bind:state onsubmit={handler} class="space-y-4">...</Form>Custom Validation
Use the validate prop for custom sync/async validation without a schema library.
<script lang="ts">
import { Form, FormField, Input, Button, toast } from 'sv5ui';
import type { FormError } from 'sv5ui';
let state = $state({ username: '', confirmPassword: '', password: '' });
function validate(st: typeof state): FormError[] {
const errors: FormError[] = [];
if (!st.username) {
errors.push({ name: 'username', message: 'Username is required' });
} else if (st.username.length < 3) {
errors.push({ name: 'username', message: 'Username must be at least 3 characters' });
}
if (!st.password) {
errors.push({ name: 'password', message: 'Password is required' });
}
if (st.password !== st.confirmPassword) {
errors.push({ name: 'confirmPassword', message: 'Passwords do not match' });
}
return errors;
}
</script>
<Form {validate} bind:state onsubmit={() => { toast.success('Registration valid!'); }} class="space-y-4">
<FormField label="Username" name="username" required>
<Input bind:value={state.username} placeholder="Choose a username" />
</FormField>
<FormField label="Password" name="password" required>
<Input bind:value={state.password} type="password" placeholder="Enter password" />
</FormField>
<FormField label="Confirm Password" name="confirmPassword" required>
<Input bind:value={state.confirmPassword} type="password" placeholder="Repeat password" />
</FormField>
<Button type="submit" label="Register" block />
</Form>Validate On Events
Control when validation triggers with validateOn. Default is ['input', 'blur', 'change'].
<script lang="ts">
import { Form, FormField, Input, Button } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
email: z.email('Invalid email'),
name: z.string().min(2, 'Name is too short')
});
let state = $state({});
</script>
<!-- Only validate on blur (not on every keystroke) -->
<Form {schema} bind:state validateOn={['blur']} onsubmit={() => {}} class="space-y-4">
<FormField label="Name" name="name">
<Input bind:value={state.name} placeholder="Your name" />
</FormField>
<FormField label="Email" name="email">
<Input bind:value={state.email} placeholder="you@example.com" />
</FormField>
<Button type="submit" label="Submit" block />
</Form>The Rule for 'focus'
Including 'focus' re-checks a field when the user comes back to it. What it no longer does is complain about a field nobody has filled in yet: arriving in a field is not an edit, and an error before the first attempt is noise rather than help.
Tab through the two fields below without typing anything. Nothing happens. Leave one invalid, come back to it, and the error returns. The second field carries eagerValidation, so it checks from the first focus.
<script lang="ts">
import { Form, FormField, Input, Button } from 'sv5ui';
</script>
<!-- 'focus' validates a field when it is focused, and since v2.7.0 it waits
for the first blur before it says anything. Tabbing through an empty
form, or opening a dialog that autofocuses its first field, no longer
greets the user with errors they have not had a chance to cause. -->
<Form {schema} bind:state validateOn={['focus', 'blur']} {onsubmit}>
<FormField label="Email" name="email">
<Input bind:value={state.email} />
</FormField>
<!-- eagerValidation opts one field back into checking from the very first
focus. It is the same prop that makes 'input' validate before the
first blur, and it now governs 'focus' as well. -->
<FormField label="Coupon" name="coupon" eagerValidation>
<Input bind:value={state.coupon} />
</FormField>
<Button type="submit" label="Submit" />
</Form>
<!-- Unchanged: the default is still ['input', 'blur', 'change'], and submit
always validates everything regardless of validateOn. -->Before and After v2.7.0
Only the first two rows changed.
| Situation | Before 2.7.0 |
|---|---|
Tab into an empty field, type nothing | Error shown immediately |
A dialog autofocuses its first field | Error shown as the dialog opens |
Leave a field invalid, then come back to it | Error shown |
Field marked eagerValidation | Error shown from the first focus |
Submit | Everything validated |
Eager Validation
By default, fields only validate after first blur. Set eagerValidation on a FormField to validate immediately on every input. Since v2.7.0 the same prop governs 'focus', so a field that should be checked from the moment it is entered says so once, in one place.
<script lang="ts">
import { Form, FormField, Input, Button } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
username: z.string().min(3, 'At least 3 characters'),
email: z.email('Invalid email')
});
let state = $state({});
</script>
<Form {schema} bind:state onsubmit={() => {}} class="space-y-4">
<!-- eagerValidation: validates immediately on input, before first blur -->
<FormField label="Username" name="username" eagerValidation>
<Input bind:value={state.username} placeholder="Type to see instant validation" />
</FormField>
<!-- Default: validates only after first blur -->
<FormField label="Email" name="email">
<Input bind:value={state.email} placeholder="Validates after you leave the field" />
</FormField>
<Button type="submit" label="Submit" block />
</Form>Validate Input Delay
Debounce input validation with validateOnInputDelay. Set at form-level or override per-field.
<script lang="ts">
import { Form, FormField, Input, Button } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
search: z.string().min(3, 'Enter at least 3 characters'),
code: z.string().regex(/^[A-Z]{3}-\d{3}$/, 'Format: ABC-123')
});
let state = $state({});
</script>
<!-- Global debounce: 500ms -->
<Form {schema} bind:state validateOnInputDelay={500} onsubmit={() => {}} class="space-y-4">
<FormField label="Search" name="search" description="Debounced at 500ms (form-level)">
<Input bind:value={state.search} placeholder="Start typing..." leadingIcon="lucide:search" />
</FormField>
<!-- Per-field override: 1000ms -->
<FormField label="Product Code" name="code" description="Debounced at 1000ms (field-level)" validateOnInputDelay={1000}>
<Input bind:value={state.code} placeholder="ABC-123" />
</FormField>
<Button type="submit" label="Search" block />
</Form>Async Submit
When onsubmit returns a Promise, the form auto-disables and sets loading to true (controlled by loadingAuto). Double-submit is prevented automatically.
<script lang="ts">
import { Form, FormField, Input, Textarea, Button } from 'sv5ui';
import type { FormApi, FormSubmitEvent } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
title: z.string().min(1, 'Title is required'),
body: z.string().min(10, 'Body must be at least 10 characters')
});
let state = $state({});
let api = $state<FormApi>();
let result = $state('');
async function onsubmit(event: FormSubmitEvent) {
// Simulate API call - form auto-disables during this
await new Promise((r) => setTimeout(r, 2000));
result = JSON.stringify(event.data, null, 2);
}
</script>
<!-- loadingAuto (default: true) disables form during async submit -->
<Form {schema} bind:state bind:api {onsubmit} class="space-y-4">
<FormField label="Title" name="title" required>
<Input bind:value={state.title} placeholder="Post title" />
</FormField>
<FormField label="Body" name="body" required>
<Textarea bind:value={state.body} placeholder="Write something..." rows={3} />
</FormField>
<Button type="submit" label={api?.loading ? 'Saving...' : 'Save Post'} loading={api?.loading} block />
{#if result}
<pre class="mt-2 rounded-lg bg-surface-container p-3 text-sm">{result}</pre>
{/if}
</Form>Programmatic API
Use bind:api to access methods like submit(), validate(), clear(), reset(), and reactive state from outside the form.
<script lang="ts">
import { Form, FormField, Input, Button, toast } from 'sv5ui';
import type { FormApi } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
firstName: z.string().min(1, 'Required'),
lastName: z.string().min(1, 'Required'),
email: z.email('Invalid email')
});
let state = $state({});
let api = $state<FormApi>();
</script>
<Form {schema} bind:state bind:api onsubmit={() => {}} class="space-y-4">
<FormField label="First Name" name="firstName" required>
<Input bind:value={state.firstName} placeholder="John" />
</FormField>
<FormField label="Last Name" name="lastName" required>
<Input bind:value={state.lastName} placeholder="Doe" />
</FormField>
<FormField label="Email" name="email" required>
<Input bind:value={state.email} placeholder="john@example.com" />
</FormField>
</Form>
<!-- External controls via bind:api -->
<div class="mt-4 flex flex-wrap gap-2">
<Button label="Submit" size="sm" onclick={() => api?.submit()} />
<Button label="Validate" size="sm" variant="outline" onclick={async () => {
const result = await api?.validate({ silent: true });
toast(result ? 'Valid!' : 'Has errors');
}} />
<Button label="Clear Errors" size="sm" variant="outline" color="warning" onclick={() => api?.clear()} />
<Button label="Reset" size="sm" variant="outline" color="error" onclick={() => api?.reset()} />
<Button label="Set Error" size="sm" variant="soft" color="error" onclick={() => {
api?.setErrors([{ name: 'email', message: 'Email already taken' }]);
}} />
</div>
{#if api}
<div class="mt-4 grid grid-cols-2 gap-2 text-sm">
<div>Dirty: <strong>{api.dirty}</strong></div>
<div>Submit count: <strong>{api.submitCount}</strong></div>
<div>Errors: <strong>{api.errors.length}</strong></div>
<div>Loading: <strong>{api.loading}</strong></div>
</div>
{/if}Async Field Validation
Combine schema with async custom validate for checks like username availability. Both run and errors are merged.
<script lang="ts">
import { Form, FormField, Input, Button, toast } from 'sv5ui';
import type { FormError } from 'sv5ui';
import { z } from 'zod';
const takenUsernames = ['admin', 'root', 'user', 'test'];
const schema = z.object({
username: z.string().min(3, 'At least 3 characters'),
email: z.email('Invalid email')
});
let state = $state({});
async function validate(st: { username?: string }): Promise<FormError[]> {
const errors: FormError[] = [];
if (st.username && takenUsernames.includes(st.username.toLowerCase())) {
// Simulate async check (e.g., API call)
await new Promise((r) => setTimeout(r, 500));
errors.push({ name: 'username', message: `"${st.username}" is already taken` });
}
return errors;
}
</script>
<!-- Combine schema + custom async validation -->
<Form {schema} {validate} bind:state onsubmit={() => { toast.success('Registered successfully!'); }} class="space-y-4">
<FormField label="Username" name="username" required hint="Try: admin, root, user, test">
<Input bind:value={state.username} placeholder="Pick a username" leadingIcon="lucide:at-sign" />
</FormField>
<FormField label="Email" name="email" required>
<Input bind:value={state.email} placeholder="you@example.com" leadingIcon="lucide:mail" />
</FormField>
<Button type="submit" label="Register" block />
</Form>Dependent Fields
Use $derived and $effect to create fields that depend on each other. Changing a parent resets its children.
<script lang="ts">
import { Form, FormField, Input, Select, Button, toast } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
country: z.string().min(1, 'Select a country'),
state: z.string().min(1, 'Select a state'),
city: z.string().min(1, 'Enter a city')
});
let state = $state({ country: '', state: '', city: '' });
const countries = [
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'uk', label: 'United Kingdom' }
];
const statesByCountry: Record<string, { value: string; label: string }[]> = {
us: [
{ value: 'ca', label: 'California' },
{ value: 'ny', label: 'New York' },
{ value: 'tx', label: 'Texas' }
],
ca: [
{ value: 'on', label: 'Ontario' },
{ value: 'bc', label: 'British Columbia' },
{ value: 'qc', label: 'Quebec' }
],
uk: [
{ value: 'eng', label: 'England' },
{ value: 'sco', label: 'Scotland' },
{ value: 'wal', label: 'Wales' }
]
};
let availableStates = $derived(statesByCountry[state.country] ?? []);
// Reset dependent fields when country changes
$effect(() => {
state.country;
state.state = '';
state.city = '';
});
</script>
<Form {schema} bind:state onsubmit={() => { toast.success('Form submitted successfully!'); }} class="space-y-4">
<FormField label="Country" name="country" required>
<Select items={countries} bind:value={state.country} placeholder="Select country" />
</FormField>
<FormField label="State / Province" name="state" required>
<Select items={availableStates} bind:value={state.state} placeholder="Select state" disabled={!state.country} />
</FormField>
<FormField label="City" name="city" required>
<Input bind:value={state.city} placeholder="Enter city" disabled={!state.state} />
</FormField>
<Button type="submit" label="Save Address" block />
</Form>Nested Forms
Use nested and name to attach a child form to a parent. The child renders as a <div> and its validation, errors, and reset cascade from the parent.
<script lang="ts">
import { Form, FormField, Input, Button, toast } from 'sv5ui';
import { z } from 'zod';
const parentSchema = z.object({
fullName: z.string().min(1, 'Name is required')
});
const addressSchema = z.object({
street: z.string().min(1, 'Street is required'),
zip: z.string().regex(/^\d{5}$/, 'ZIP must be 5 digits')
});
let state = $state({
fullName: '',
address: { street: '', zip: '' }
});
</script>
<!-- Parent form -->
<Form schema={parentSchema} bind:state onsubmit={() => { toast.success('Form submitted successfully!'); }} class="space-y-4">
<FormField label="Full Name" name="fullName" required>
<Input bind:value={state.fullName} placeholder="John Doe" />
</FormField>
<!-- Nested form - renders as <div>, attaches to parent -->
<Form nested name="address" schema={addressSchema}>
<fieldset class="space-y-4 rounded-lg border border-on-surface/15 p-4 pt-0">
<legend class="px-2 text-sm font-medium">Address</legend>
<FormField label="Street" name="street" required>
<Input bind:value={state.address.street} placeholder="123 Main St" />
</FormField>
<FormField label="ZIP Code" name="zip" required>
<Input bind:value={state.address.zip} placeholder="12345" />
</FormField>
</fieldset>
</Form>
<Button type="submit" label="Submit All" block />
</Form>Error Summary
Access all errors via bind:api to render an error summary list at the top of the form.
<script lang="ts">
import { Form, FormField, Input, Button, Alert, toast } from 'sv5ui';
import type { FormApi } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.email('Invalid email address'),
phone: z.string().regex(/^\+?\d{10,15}$/, 'Invalid phone number'),
website: z.url('Invalid URL').or(z.literal(''))
});
let state = $state({});
let api = $state<FormApi>();
</script>
<Form {schema} bind:state bind:api onsubmit={() => { toast.success('Contact saved!'); }} class="space-y-4">
{#if api?.errors.length}
<Alert color="error" icon="lucide:alert-circle" title="Please fix the following errors:">
<ul class="list-inside list-disc text-sm">
{#each api.errors as err}
<li>{err.message}</li>
{/each}
</ul>
</Alert>
{/if}
<FormField label="Name" name="name" required>
<Input bind:value={state.name} placeholder="Full name" />
</FormField>
<FormField label="Email" name="email" required>
<Input bind:value={state.email} placeholder="you@example.com" />
</FormField>
<FormField label="Phone" name="phone" required>
<Input bind:value={state.phone} placeholder="+1234567890" />
</FormField>
<FormField label="Website" name="website">
<Input bind:value={state.website} placeholder="https://example.com" />
</FormField>
<Button type="submit" label="Save Contact" block />
</Form>Array / Repeater Fields
Use a nested <Form> per array item with its own schema. Each nested form validates independently and cascades errors to the parent on submit.
<script lang="ts">
import { Form, FormField, Input, Button, toast } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
customer: z.string().min(2, 'Customer name is required')
});
const itemSchema = z.object({
description: z.string().min(1, 'Description is required'),
price: z.string().regex(/^\d+(\.\d{1,2})?$/, 'Enter a valid price')
});
interface Item { description: string; price: string }
let state = $state<{ customer: string; items: Item[] }>({
customer: '',
items: [{ description: '', price: '' }]
});
function addItem() {
state.items = [...state.items, { description: '', price: '' }];
}
function removeItem() {
if (state.items.length > 0) {
state.items = state.items.slice(0, -1);
}
}
</script>
<Form {schema} bind:state onsubmit={() => { toast.success('Form submitted successfully!'); }} class="space-y-4">
<FormField label="Customer" name="customer" required>
<Input bind:value={state.customer} placeholder="Wonka Industries" />
</FormField>
<!-- Each array item gets its own nested Form with independent schema -->
{#each state.items as item, i (i)}
<Form nested name={`items.${i}`} schema={itemSchema} class="flex gap-2">
<FormField label={i === 0 ? 'Description' : undefined} name="description" class="flex-1">
<Input bind:value={item.description} placeholder="Item description" size="sm" />
</FormField>
<FormField label={i === 0 ? 'Price' : undefined} name="price" class="w-24">
<Input bind:value={item.price} placeholder="0.00" size="sm" />
</FormField>
</Form>
{/each}
<div class="flex gap-2">
<Button size="sm" variant="outline" label="Add Item" leadingIcon="lucide:plus" onclick={addItem} />
{#if state.items.length > 1}
<Button size="sm" variant="ghost" color="error" label="Remove" leadingIcon="lucide:minus" onclick={removeItem} />
{/if}
</div>
<Button type="submit" label="Submit Invoice" block />
</Form>Disabled
Set disabled to disable the entire form and all its inputs.
<script lang="ts">
import { Form, FormField, Input, Switch, Button } from 'sv5ui';
let disabled = $state(true);
let state = $state({ name: 'John Doe', email: 'john@example.com' });
</script>
<div class="mb-4 flex items-center gap-2">
<Switch bind:checked={disabled} size="sm" />
<span class="text-sm">Form disabled</span>
</div>
<Form {disabled} bind:state onsubmit={() => {}} class="space-y-4">
<FormField label="Name" name="name">
<Input bind:value={state.name} />
</FormField>
<FormField label="Email" name="email">
<Input bind:value={state.email} />
</FormField>
<Button type="submit" label="Save" block />
</Form>Schema Transform
When transform is true (default), Zod .transform() and Valibot pipe transforms are applied to event.data on submit.
<script lang="ts">
import { Form, FormField, Input, Button } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
email: z.email().transform((v) => v.toLowerCase().trim()),
age: z.string().transform((v) => Number(v)).pipe(z.number().min(18, 'Must be 18+'))
});
let state = $state({});
let result = $state('');
</script>
<!-- transform (default: true) applies schema .transform() on submit -->
<Form {schema} bind:state onsubmit={(e) => { result = JSON.stringify(e.data, null, 2); }} class="space-y-4">
<FormField label="Email" name="email" required description="Will be lowercased and trimmed">
<Input bind:value={state.email} placeholder="USER@Example.COM" />
</FormField>
<FormField label="Age" name="age" required description="String input transformed to number">
<Input bind:value={state.age} placeholder="25" />
</FormField>
<Button type="submit" label="Submit (with transform)" block />
{#if result}
<div class="rounded-lg bg-surface-container p-3">
<p class="mb-1 text-xs font-medium text-on-surface/60">Transformed output:</p>
<pre class="text-sm">{result}</pre>
</div>
{/if}
</Form>Error Event
The onerror callback fires when validation fails on submit. Use it for logging, analytics, or custom error display.
<script lang="ts">
import { Form, FormField, Input, Button, Alert } from 'sv5ui';
import type { FormErrorEvent, FormErrorWithId } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
email: z.email('Invalid email'),
password: z.string().min(8, 'At least 8 characters')
});
let state = $state({});
let lastErrors = $state<FormErrorWithId[]>([]);
function onerror(event: FormErrorEvent) {
lastErrors = event.errors;
console.log('Validation failed:', event.errors);
}
</script>
<Form {schema} bind:state {onerror} onsubmit={() => { lastErrors = []; }} class="space-y-4">
<FormField label="Email" name="email" required>
<Input bind:value={state.email} placeholder="you@example.com" />
</FormField>
<FormField label="Password" name="password" required>
<Input bind:value={state.password} type="password" placeholder="Min 8 characters" />
</FormField>
<Button type="submit" label="Login" block />
</Form>
{#if lastErrors.length > 0}
<Alert color="warning" icon="lucide:bug" title="onerror fired" class="mt-4">
<pre class="text-xs">{JSON.stringify(lastErrors, null, 2)}</pre>
</Alert>
{/if}All Input Types
Form works seamlessly with all sv5ui input components: Input, Textarea, Select, RadioGroup, Checkbox, Slider, and more.
<script lang="ts">
import { Form, FormField, Input, Textarea, Select, Checkbox, RadioGroup, Slider, Button, toast } from 'sv5ui';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.email('Invalid email'),
bio: z.string().max(200, 'Max 200 characters').optional(),
role: z.string().min(1, 'Select a role'),
experience: z.number().min(0).max(20),
newsletter: z.boolean(),
contactMethod: z.string().min(1, 'Pick one')
});
let state = $state({
name: '', email: '', bio: '', role: '',
experience: 5, newsletter: false, contactMethod: ''
});
const roles = [
{ value: 'dev', label: 'Developer' },
{ value: 'design', label: 'Designer' },
{ value: 'pm', label: 'Product Manager' }
];
const contactMethods = [
{ value: 'email', label: 'Email' },
{ value: 'phone', label: 'Phone' },
{ value: 'slack', label: 'Slack' }
];
</script>
<Form {schema} bind:state onsubmit={() => { toast.success('Form submitted successfully!'); }} class="space-y-4">
<FormField label="Name" name="name" required hint="As it appears on your ID">
<Input bind:value={state.name} placeholder="Jane Smith" leadingIcon="lucide:user" />
</FormField>
<FormField label="Email" name="email" required>
<Input bind:value={state.email} placeholder="jane@company.com" leadingIcon="lucide:mail" />
</FormField>
<FormField label="Bio" name="bio" description="Short description about yourself" help="Max 200 characters">
<Textarea bind:value={state.bio} placeholder="Tell us about yourself..." rows={3} />
</FormField>
<FormField label="Role" name="role" required>
<Select items={roles} bind:value={state.role} placeholder="Select your role" />
</FormField>
<FormField label="Years of Experience" name="experience">
<Slider bind:value={state.experience} min={0} max={20} step={1} />
</FormField>
<FormField label="Preferred Contact" name="contactMethod" required>
<RadioGroup items={contactMethods} bind:value={state.contactMethod} orientation="horizontal" />
</FormField>
<FormField name="newsletter">
<Checkbox bind:checked={state.newsletter} label="Subscribe to newsletter" />
</FormField>
<Button type="submit" label="Save Profile" block />
</Form>Snippets
Named snippets exposed by the Form component.
| Snippet | Description |
|---|---|
children | Default slot - optionally receives { errors, loading } props via {#snippet children({ errors, loading })}. |
UI Slots
Use the ui prop to override classes on internal elements.
| Slot | Description |
|---|---|
root | Root element - <form> by default, <div> when nested=true. |
Props
| Prop | Type | Default |
|---|---|---|
schema | StandardSchemaV1 | JoiSchema | - |
state | Partial<InferInput<S>> | {} |
validate | (state) => FormError[] | Promise<FormError[]> | - |
validateOn | FormInputEvents[] | ['input','blur','change'] |
validateOnInputDelay | number | 300 |
disabled | boolean | false |
loadingAuto | boolean | true |
transform | boolean | true |
nested | boolean | false |
name | string | - |
id | string | number | auto UUID |
api | FormApi | - |
ref | HTMLElement | null | - |
onsubmit | (event: FormSubmitEvent) => void | Promise | - |
onerror | (event: FormErrorEvent) => void | - |
class | string | - |
ui | Partial<Record<'root', Class>> | - |
FormApi (bind:api)
Methods and reactive properties available via bind:api.
| Name | Type | Default |
|---|---|---|
submit() | () => Promise<void> | - |
validate(opts?) | (opts?) => Promise<T | false> | - |
clear(name?) | (name?: string | RegExp) => void | - |
reset() | () => void | - |
setErrors(errs, name?) | (errs: FormError[], name?: string | RegExp) => void | - |
getErrors(name?) | (name?: string | RegExp) => FormErrorWithId[] | - |
errors | FormErrorWithId[] | - |
loading | boolean | - |
disabled | boolean | - |
dirty | boolean | - |
dirtyFields | ReadonlySet<string> | - |
touchedFields | ReadonlySet<string> | - |
blurredFields | ReadonlySet<string> | - |
submitCount | number | - |
Advanced Exports
Building a custom input component or low-level integration? sv5ui re-exports a few internals you'll occasionally need. These are stable public exports - not implementation details.
FORM_FIELD_CONTEXT_KEY
unique symbol
The Symbol used as the FormField context key. Exported so consumers can call getContext(FORM_FIELD_CONTEXT_KEY) directly when reading inside a non-component setup, or to namespace their own context-aware utilities. Most code should use useFormField() instead.
getFormContext<T>()
() => FormContext<T> | undefined
Returns the parent Form's reactive FormContext instance - exposing errors, loading, submitCount, dirty, disabled, dirty/touched/blurred field sets, and methods like setErrors(), clear(), submit(), validate(). Returns undefined when used outside a Form. Used internally by Form-aware components that need richer state than useFormField() exposes.
InputValue / InputProps<T>
type InputValue = string | number | bigint | boolean | null | undefined
InputValue is the union of every primitive sv5ui's <Input> can bind:value to. InputProps<T extends InputValue> is the generic prop shape - use it when wrapping <Input> in a typed adapter so callers keep type-safe bind:value inference.
- useFormField() - read FormField context (name, ariaId, error, size).
- useFormFieldEmit() - emit blur/focus/input/change events to the parent Form.