ShardsUI is in beta. APIs may change before 1.0.

Form

A validated form.

<script lang="ts">
  import { Button } from '@shardsui/svelte/button'
  import { Field } from '@shardsui/svelte/field'
  import { Form, type FormErrors } from '@shardsui/svelte/form'

  let errors = $state<FormErrors>({})
  let loading = $state(false)

  async function joinWaitlist(email: string): Promise<FormErrors> {
    await new Promise((resolve) => setTimeout(resolve, 1000))
    if (email.endsWith('@example.com')) {
      return { email: 'This email is already on the waitlist' }
    }
    return {}
  }
</script>

<Form
  class="flex w-full max-w-64 flex-col gap-4"
  {errors}
  onsubmit={async (event) => {
    event.preventDefault()
    const email = new FormData(event.currentTarget).get('email')
    loading = true
    errors = await joinWaitlist(String(email))
    loading = false
  }}
>
  <Field.Root name="email" class="flex flex-col items-start gap-1">
    <Field.Label class="text-sm font-semibold text-gray-900">Email</Field.Label>
    <Field.Control
      type="email"
      required
      placeholder="you@company.com"
      class="h-8 w-full rounded-md border border-gray-200 px-2 text-sm font-normal text-gray-900 focus:outline-2 focus:-outline-offset-1 focus:outline-gray-950 any-pointer-coarse:text-base"
    />
    <Field.Error class="text-sm text-red-800" />
  </Field.Root>
  <Button
    type="submit"
    disabled={loading}
    class="font-inherit m-0 flex h-8 items-center justify-center gap-2 rounded-md border border-gray-200 bg-gray-50 px-3 text-sm/6 font-normal text-nowrap text-gray-900 outline-0 select-none hover:bg-gray-100 focus-visible:outline-2 focus-visible:-outline-offset-1 focus-visible:outline-gray-950 data-disabled:text-gray-500 hover:data-disabled:bg-gray-50"
  >
    Join waitlist
  </Button>
</Form>

Anatomy

Pair Form with Field:

<script>
  import { Form } from '@shardsui/svelte/form'
  import { Field } from '@shardsui/svelte/field'
</script>

<Form>
  <Field.Root>
    <Field.Label />
    <Field.Control />
    <Field.Error />
  </Field.Root>
</Form>

Examples

Validating from code

The component instance, obtained with bind:this, exposes validate(). It runs every field; pass a field name to run just that one.

<script>
  let form = $state()
</script>

<Form bind:this={form}>
  <Field.Root name="email">
    <Field.Control type="email" required />
    <Field.Error />
  </Field.Root>
  <button type="button" onclick={() => form.validate('email')}>Check email</button>
</Form>

The name matched is the one on Field.Root, falling back to the name on the control.

API reference

Renders a <form> element. Submitting validates every field first; if one fails, the native submit is cancelled and focus moves to the first invalid control, selecting its text when it is an <input>.

PropTypeDefault

The component instance, obtained with bind:this, exposes:

MethodDescription
validate(fieldName?: string)Runs validation. With fieldName, only that field — matched against Field.Root's name, falling back to the control's name. Without it, every field.

<Form> is generic over FormValues extends Record<string, unknown>, which defaults to Record<string, unknown>. Set it to type the object onFormSubmit receives.

Other standard <form> attributes (method, action, target, enctype) pass through.