Forms

FormField

Wrap any form control with labels, descriptions, hints, error messages, and help text. Provides context to child components for automatic error handling and sizing.

Playground

Experiment with different props in real-time.

Required

Your work email

We'll send a confirmation

Basic Usage

Wrap a form control with a label.

<script lang="ts">
  import { FormField, Input } from 'sv5ui';
</script>

<FormField label="Email">
  <Input placeholder="you@example.com" />
</FormField>

Description

Add context below the label.

Must be at least 8 characters

<FormField label="Password" description="Must be at least 8 characters">
  <Input type="password" placeholder="Enter password" />
</FormField>

Hint

Display a hint next to the label (right side).

Required
Optional
<FormField label="Username" hint="Required">
  <Input placeholder="johndoe" />
</FormField>

<FormField label="Bio" hint="Optional">
  <Textarea placeholder="Tell us about yourself..." />
</FormField>

Help Text

Display help text below the control.

Include the full URL with https://

<FormField label="Website" help="Include the full URL with https://">
  <Input placeholder="https://example.com" leadingIcon="lucide:globe" />
</FormField>

Error

Error message replaces help text and auto-highlights the child control.

Please enter a valid email address

Password is too short

<!-- String error message -->
<FormField label="Email" error="Please enter a valid email address">
  <Input placeholder="you@example.com" />
</FormField>

<!-- Boolean error (no message) -->
<FormField label="Name" error={true}>
  <Input placeholder="Your name" />
</FormField>

<!-- Error replaces help text -->
<FormField label="Password" help="Must be 8+ chars" error="Password is too short">
  <Input type="password" />
</FormField>

Required

Adds a red asterisk to the label and sets aria-required on the text controls inside: Input, InputNumber, InputTags and Textarea. Other controls take their own required prop.

We'll never share your email
<FormField label="Full Name" required>
  <Input placeholder="John Doe" />
</FormField>

<FormField label="Email" required hint="We'll never share your email">
  <Input type="email" placeholder="you@example.com" />
</FormField>

Sizes

Controls label and text size. Also propagates to child components.

<FormField label="Extra Small" size="xs">
  <Input placeholder="XS input" />
</FormField>

<FormField label="Small" size="sm">
  <Input placeholder="SM input" />
</FormField>

<FormField label="Medium" size="md">
  <Input placeholder="MD input" />
</FormField>

<FormField label="Large" size="lg">
  <Input placeholder="LG input" />
</FormField>

Horizontal

Side-by-side label and control layout.

<FormField label="Name" orientation="horizontal">
  <Input placeholder="John Doe" />
</FormField>

<FormField label="Email" orientation="horizontal" required>
  <Input type="email" placeholder="you@example.com" />
</FormField>

<FormField label="Role" orientation="horizontal">
  <Select items={roles} placeholder="Select role" />
</FormField>

With Input

Find anything in your workspace

USD
<FormField label="Search" description="Find anything in your workspace">
  <Input icon="lucide:search" placeholder="Search..." />
</FormField>

<FormField label="Amount" hint="USD">
  <Input type="number" leadingIcon="lucide:dollar-sign" placeholder="0.00" />
</FormField>

With Textarea

Describe your issue in detail

Markdown supported

<FormField label="Message" description="Describe your issue in detail" help="Markdown supported">
  <Textarea placeholder="Type your message..." autoresize />
</FormField>

With Select & SelectMenu

Choose your preferred framework

Search frameworks...
<FormField label="Country" required>
  <Select items={countries} placeholder="Select a country" />
</FormField>

<FormField label="Framework" description="Choose your preferred framework">
  <SelectMenu items={frameworks} placeholder="Search frameworks..." />
</FormField>

With CheckboxGroup

<FormField label="Preferences">
  <CheckboxGroup items={[
    { value: 'email', label: 'Email notifications' },
    { value: 'sms', label: 'SMS notifications' },
    { value: 'push', label: 'Push notifications' }
  ]} />
</FormField>

With Switch

Toggle dark theme

<FormField label="Dark Mode" description="Toggle dark theme">
  <Switch />
</FormField>

Complete Form

A real-world registration form combining multiple form components.

We'll never share it

Must be at least 8 characters

Optional
<form class="space-y-6">
  <FormField label="Full Name" required>
    <Input placeholder="John Doe" />
  </FormField>

  <FormField label="Email" required hint="We'll never share it">
    <Input type="email" placeholder="you@example.com" leadingIcon="lucide:mail" />
  </FormField>

  <FormField label="Password" required description="Must be at least 8 characters">
    <Input type="password" placeholder="Enter password" />
  </FormField>

  <FormField label="Role">
    <Select items={roles} placeholder="Select role" />
  </FormField>

  <FormField label="Bio" hint="Optional">
    <Textarea placeholder="Tell us about yourself..." autoresize />
  </FormField>

  <FormField label="Notifications">
    <Switch label="Receive email updates" />
  </FormField>

  <Button type="submit" label="Create Account" block />
</form>

Accessibility

A field wires its control up for assistive technology without any props from you. The demo reads the attributes back from the input, so you can watch them change.

Shown on your profile

3 to 20 characters

0 / 20

aria-describedby
aria-invalid
not set
aria-required
not set
<script lang="ts">
  import { FormField, Input } from 'sv5ui';

  let username = $state('');
</script>

<FormField
  name="username"
  label="Username"
  description="Shown on your profile"
  help="3 to 20 characters"
  required
>
  <!-- Your id is merged with the field's ids, yours first -->
  <Input bind:value={username} aria-describedby="username-count" />
</FormField>

<p id="username-count">{username.length} / 20</p>
  • Name. The label names the control, through for where a label can point at it and through aria-labelledby where it cannot. An aria-label or aria-labelledby you pass to the control wins.
  • Description. aria-describedby lists the description and the help text, and only the ones that are rendered. When an error appears it takes the place of the help text, the description stays, and the control gets aria-invalid.
  • Your own attributes. An aria-describedby on the control is merged with the field's ids, yours first, and an aria-invalid you set is kept. Event handlers you pass run alongside the field's own.
  • Your own id. Give the control an id and the label follows it, as do the errors of a surrounding Form.
  • Slots. Content passed to descriptionSlot, helpSlot and errorSlot is announced too. Each is rendered inside a wrapper with display: contents that carries the id, so layout is unchanged but a direct child selector now meets the wrapper first.

A control with no visible label needs a name from you. Inside a field the label provides it:

Applies to this screen only

<!-- On its own: name the control yourself. The attribute lands on the thumb -->
<Slider aria-label="Volume" value={40} />

<!-- In a field: the label names it, and the description is announced -->
<FormField label="Brightness" description="Applies to this screen only">
  <Slider value={70} />
</FormField>

Controls That Follow These Rules

How the field label reaches each control, and whether required on the field sets aria-required on it.

ControlNamed througharia-required from the field
Inputlabel forYes
InputNumberlabel forYes
InputTagslabel forYes
Textarealabel forYes
Selectlabel forNo
Checkboxlabel forNo
Switchlabel forNo
SelectMenuaria-labelledbyNo
Ratingaria-labelledbyNo
Slideraria-labelledby on each thumbNo
RadioGrouparia-labelledby, unless it has a legendNo
CheckboxGrouparia-labelledby, unless it has a legendNo
ColorPickeraria-labelledby on the groupNo

InputPassword is built on Input and behaves the same. The other controls that read a field (Calendar, DatePicker, DateRangePicker, TimeField, PinInput, FileUpload, Editor and ImageCropper) have not moved to these rules yet, so do not rely on merged attributes there.

UI Slots

Use the ui prop to override classes on internal elements.

SlotDescription
rootOutermost wrapper
wrapperContains label section and description
labelWrapperFlex container for label and hint
labelLabel text element
descriptionDescription text below label
hintHint text next to label
containerContains form control + error/help
errorError message text
helpHelp text below control

Snippets

SnippetDescription
childrenForm control content (receives error state)
labelSlotCustom label rendering
hintSlotCustom hint rendering
descriptionSlotCustom description rendering. Wrapped in an element that carries the description id
helpSlotCustom help text rendering. Wrapped in an element that carries the help id
errorSlotCustom error rendering. Wrapped in an element that carries the error id

Props

PropTypeDefault
labelstring-
descriptionstring-
hintstring-
helpstring-
errorstring | boolean-
namestring-
eagerValidationbooleanfalse
validateOnInputDelaynumber-
size'xs' | 'sm' | 'md' | 'lg' | 'xl''md'
orientation'vertical' | 'horizontal''vertical'
requiredbooleanfalse
refHTMLElement | nullnull
classstring-
uiRecord<Slot, Class>-