Name any control with a label, and let a press on the label reach it.

A plain <label> only reaches inputs, buttons and selects. Label names any control by its for, including a radio group or a slider, and a press on the label moves focus into it.

<script lang="ts">
  import { Label } from "@ui-sdk/svelte/label"
  import { Radio, RadioGroup, RadioIndicator } from "@ui-sdk/svelte/radio"

  const speeds = [
    { value: "standard", label: "Standard" },
    { value: "express", label: "Express" },
    { value: "overnight", label: "Overnight" },
  ]
</script>

<div data-slot="stack">
  <Label for="shipping-speed">Shipping speed</Label>
  <RadioGroup id="shipping-speed" defaultValue="standard">
    {#each speeds as speed (speed.value)}
      <Label>
        <Radio value={speed.value}>
          <RadioIndicator />
        </Radio>
        {speed.label}
      </Label>
    {/each}
  </RadioGroup>
</div>
Press Shipping speed. Focus moves to the checked option, and a screen reader names the group Shipping speed.

Installation

npm install @ui-sdk/svelte @ui-sdk/core

Usage

Import the label and point its for at the control's id:

<script lang="ts">
  import { Label } from "@ui-sdk/svelte/label"
  import { Radio, RadioGroup } from "@ui-sdk/svelte/radio"
</script>

<Label for="plan">Plan</Label>
<RadioGroup id="plan" defaultValue="pro">
  <Radio value="free">Free</Radio>
  <Radio value="pro">Pro</Radio>
</RadioGroup>

Label renders a native <label>. When the browser can reach the control on its own, like an input, it does. For anything else, the control takes the label as its name, and a press on the label focuses it, checks it or clicks it, as a native label would.

Inside a Field, use FieldLabel, which also ties the field's description and errors to the control.

Build Your Own

@ui-sdk/svelte/label comes with a ready-made Label. If you'd like to build your own, with your own name, start from the label primitive in @ui-sdk/core:

caption.ts
import { label } from "@ui-sdk/core/label"
import { createParts } from "@ui-sdk/svelte/primitive"

const { Root: Caption } = createParts(label, { root: "label" })

export { Caption }
<Caption for="plan">Plan</Caption>

Caption keeps the behavior of the ready-made Label: it names the control and reaches it on a press. See Rendering and Custom Parts for more.

Slider

A label for a slider names its thumb, so the thumb reads as Volume, and a press on the label focuses it.

<script lang="ts">
  import { Label } from "@ui-sdk/svelte/label"
  import {
    Slider,
    SliderControl,
    SliderIndicator,
    SliderThumb,
    SliderTrack,
    SliderValue,
  } from "@ui-sdk/svelte/slider"
</script>

<Slider id="volume" defaultValue={75}>
  <div data-slot="row">
    <Label for="volume">Volume</Label>
    <SliderValue />
  </div>
  <SliderControl>
    <SliderTrack>
      <SliderIndicator />
      <SliderThumb />
    </SliderTrack>
  </SliderControl>
</Slider>
Press Volume, then use the arrow keys. The thumb moves, and a screen reader reads it as Volume.

Switch

Point the label at a switch beside it. A press on the label turns the switch on and off.

<script lang="ts">
  import { Label } from "@ui-sdk/svelte/label"
  import { Switch, SwitchThumb } from "@ui-sdk/svelte/switch"
</script>

<div data-slot="row">
  <Switch id="updates">
    <SwitchThumb />
  </Switch>
  <Label for="updates">Email me product updates</Label>
</div>
Press Email me product updates. The switch turns on.

Wrapping a Control

Put the control inside the label instead of pointing at it, and you need no id.

<script lang="ts">
  import { CheckIcon } from "@lucide/svelte"
  import { Checkbox, CheckboxIndicator } from "@ui-sdk/svelte/checkbox"
  import { Label } from "@ui-sdk/svelte/label"
</script>

<Label>
  <Checkbox>
    <CheckboxIndicator>
      <CheckIcon aria-hidden="true" />
    </CheckboxIndicator>
  </Checkbox>
  Accept terms and conditions
</Label>
Press Accept terms and conditions. The checkbox checks, and a double click doesn't select the text.

Custom Rendering

Pass a render snippet to use a different element, such as a <span>, and spread the supplied props onto it. A label that isn't a <label> still names its control and reaches it on a press. See Parts and Composition for more.

Styling

The label is unstyled. It has a data-slot of label.

A double click on the label doesn't select its text, so you don't need user-select: none for that.

API Reference

Props are the same as below, as Svelte props, along with a native label's for.

Label

debug?boolean
Logs every state change with its reason to the console, in development builds.

Accessibility

A native <label> names the element its for points at only when that element is labelable, like an input or a button. Label names any other control through aria-labelledby, unless the control has an aria-label or an aria-labelledby of its own, which win. Its press does what a native label's does: it checks a checkbox, a switch or a radio, focuses a group's checked radio, and focuses anything else. A press on a disabled control does nothing.

Tested Behavior

Every line below is an automated test.

  • A label names a native input, and a press focuses it.
  • A label names a radio group, and a press focuses its checked radio.
  • A label for a slider names its thumb, and a press focuses it.
  • A label around a checkbox names it, and a press toggles it.
  • A press on the label of a disabled group moves no focus.
  • A control its author names keeps that name.
  • A label names a control that renders after it.
  • A double click on a label doesn't select its text.

Runs in React, Vue and web components.