Forms

Form

Centralized form validation and submission. Supports schema validation (Zod, Valibot, Yup, Joi), custom validators, async submit, nested forms, programmatic API, and more.

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>

Eager Validation

By default, fields only validate after first blur. Set eagerValidation on a FormField to validate immediately on every input.

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

Debounced at 500ms (form-level)

Debounced at 1000ms (field-level)

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

Dirty: false
Submit count: 0
Errors: 0
Loading: false
<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.

Try: admin, root, user, test
<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.

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

Form disabled
<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.

Will be lowercased and trimmed

String input transformed to number

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

As it appears on your ID

Short description about yourself

Max 200 characters

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

SnippetDescription
childrenDefault slot — optionally receives { errors, loading } props via {#snippet children({ errors, loading })}.

UI Slots

Use the ui prop to override classes on internal elements.

SlotDescription
rootRoot element — <form> by default, <div> when nested=true.

Props

PropTypeDefault
schemaStandardSchemaV1 | JoiSchema-
statePartial<InferInput<S>>{}
validate(state) => FormError[] | Promise<FormError[]>-
validateOnFormInputEvents[]['input','blur','change']
validateOnInputDelaynumber300
disabledbooleanfalse
loadingAutobooleantrue
transformbooleantrue
nestedbooleanfalse
namestring-
idstring | numberauto UUID
apiFormApi-
refHTMLElement | null-
onsubmit(event: FormSubmitEvent) => void | Promise-
onerror(event: FormErrorEvent) => void-
classstring-
uiPartial<Record<'root', Class>>-

FormApi (bind:api)

Methods and reactive properties available via bind:api.

NameTypeDefault
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[]-
errorsFormErrorWithId[]-
loadingboolean-
disabledboolean-
dirtyboolean-
dirtyFieldsReadonlySet<string>-
touchedFieldsReadonlySet<string>-
blurredFieldsReadonlySet<string>-
submitCountnumber-