Forms

Field

Keep a control, its label, and its feedback connected.

Installation

pnpm dlx @mielui/svelte add field input

Usage

Field connects a label, one control, and its feedback; the control keeps its own value. Spread the attributes from Field.Control onto the component that renders the actual input, button, or other labelable control.

<script lang="ts">
    import * as Field from '@mielui/svelte/components/field';
    import { Input } from '@mielui/svelte/components/input';
</script>

<Field.Root class="w-full max-w-sm" required>
    <Field.Label>Work email</Field.Label>
    <Field.Control>
        {#snippet children(control)}
            <Input
                {...control}
                type="email"
                name="email"
                placeholder="you@company.com"
                autocomplete="email"
            />
        {/snippet}
    </Field.Control>
    <Field.Description>Weโ€™ll only use this address for account updates.</Field.Description>
</Field.Root>

Root creates a stable control ID that Label targets, even before hydration. Mounted descriptions and errors are added to the controlโ€™s aria-describedby alongside any IDs you pass yourself. Omit either part, reorder them, or add more than one description. Set controlId on Root for a custom ID.

Control renders its snippet with no wrapper element. When you write your own wrapper component, forward the supplied attachment to the real control. Input, Textarea, and components with an Input part already do. If the control renders its own label, use only one of the two labels.

Server-rendered feedback

For a form that works without JavaScript, declare the description and error IDs explicitly. Pass describedBy and errorId to Control, and matching IDs to Description and Error. Control appends errorId only while Root is invalid. Keep explicitly referenced parts present whenever those IDs apply; omit the prop when you omit its part. Automatic metadata discovery enhances the mounted form but does not replace these server-rendered relationships.

<Field.Root {issues}>
    <Field.Label>Email</Field.Label>
    <Field.Control describedBy="email-help" errorId="email-error">
        {#snippet children(control)}
            <Input {...control} name="email" type="email" />
        {/snippet}
    </Field.Control>
    <Field.Description id="email-help">Your work address.</Field.Description>
    <Field.Error id="email-error" />
</Field.Root>

Use $props.id() to prefix these IDs when a reusable form can appear more than once.

Validation feedback

Pass issues as a readonly array of objects with a message. Root derives invalid state; Error displays the messages and removes duplicates. SvelteKit remote field issues fit this shape directly. Field does not invent validation rules, submit data, or clear your errors while someone edits.

Error reveals and collapses with the short press motion token, skips movement with reduced motion, and exposes validation text immediately. It announces local updates politely by default. When a focused form-wide summary owns error feedback, set aria-live="off" on Error to avoid repeating the same issues.

Use invalid for externally managed validity. Error can accept its own issues or custom children. Keep the Root validity state in sync when supplying a custom error. Error text describes the control; use Form.Status for a single submission announcement instead of announcing every field as an alert.

A control beside its label

Use horizontal orientation for a checkbox or switch. Content keeps the label and description together while the control remains a separate region.

Composite controls

NumberField.Root owns its value, native name, range, and stepper state. Give Field.Root and NumberField.Root the same disabled and required values, then spread Control attributes onto NumberField.Input. Field.Label replaces NumberField.Label here. OTPField.Root forwards these attributes to its real input, so spread them directly onto that root. Neither example adds a second input or duplicates the label.

Groups and required fields

Group supplies spacing between fields; it adds no ARIA role. For a named group of controls or group-wide disabled behavior, use Fieldset instead. Root's disabled and required props reach the control through Field.Control. When Root is required, Field.Label shows a required mark after its text. The mark is hidden from assistive technology, which reads the control's required state instead.

Every part except Control accepts the native attributes of the element it renders: Label renders a label, Description a p, and Root, Error, Content, and Group a div. Root supports bind:element. Compose Form for pending feedback, submission, and a complete remote-form example.

API reference

Field.Root

  • controlId
    string

    Id shared by the label and the control.

  • disabled
    boolean

    Prevents interaction and dims the control.

    Default false

  • required
    boolean

    Requires a value before the form can be submitted.

    Default false

  • invalid
    boolean

    Marks the field invalid. It defaults to true when there are issues.

  • issues
    readonly Readonly<{ message: string; }>[]

    Validation issues for this field.

    Default []

  • orientation
    "vertical" | "horizontal"

    vertical stacks the label and control. horizontal puts them side by side.

    Default 'vertical'

  • element Bindable
    HTMLDivElement

    Bindable reference to the DOM element.

Field.Control

  • describedBy
    string

    Ids of the description and error, for aria-describedby.

  • errorId
    string

    Id of the error element.

  • children Required
    Snippet<[FieldControlAttributes]>

    Content rendered inside.

Field.Error

  • issues
    readonly Readonly<{ message: string; }>[]

    Validation issues to list. Defaults to the Root's issues.

Other parts

These parts have no props of their own. They pass HTML attributes and events to their element.

  • Field.Content
  • Field.Description
  • Field.Group
  • Field.Label