Skip to main content
Markdown

Switch

Switch lets users turn a single setting on or off, especially for preferences, permissions, and form options. The root owns visual state (size, variant, plus inherited invalid / disabled / required / optional / readOnly), Switch.Indicator is the track + sliding thumb. Wrap Switch in a Field to attach a label, description, or error message — the field-level state cascades into the switch automatically.

Usage

import { Field, Switch } from '@takeoff-ui/react-spar';
<Field>
<Field.Label />
<Switch>
<Switch.Indicator />
</Switch>
<Field.Description />
<Field.ErrorMessage />
</Field>

Playground


function PlaygroundDemo() {
  return (
    <Field className="max-w-90">
      <div className="flex w-full items-center justify-between">
        <div>
          <Field.Label>Flight status alerts</Field.Label>
          <Field.Description>We will let you know about delays, gate changes, and cancellations.</Field.Description>
        </div>
        <Switch defaultChecked>
          <Switch.Indicator />
        </Switch>
      </div>
    </Field>
  );
}

render(<PlaygroundDemo />);

Using with Field

Field is the generic ARIA wrapper. Setting invalid, disabled, required, optional, or readOnly on Field cascades into the nested Switch, and Field.Label, Field.Description, and Field.ErrorMessage are wired to the switch through shared IDs.


function FieldWithSwitchDemo() {
  return (
    <div className="grid w-full max-w-90 gap-6">
      <Field required>
        <div className="flex w-full items-center justify-between">
          <Field.Label>Email alerts</Field.Label>
          <Switch defaultChecked>
            <Switch.Indicator />
          </Switch>
        </div>
      </Field>

      <Field>
        <div className="flex w-full items-center justify-between">
          <div>
            <Field.Label>SMS alerts</Field.Label>
            <Field.Description>Standard messaging rates may apply.</Field.Description>
          </div>
          <Switch>
            <Switch.Indicator />
          </Switch>
        </div>
      </Field>

      <Field invalid>
        <div className="flex w-full items-center justify-between">
          <div>
            <Field.Label>Push notifications</Field.Label>
            <Field.ErrorMessage>Enable browser notifications to turn this on.</Field.ErrorMessage>
          </div>
          <Switch>
            <Switch.Indicator />
          </Switch>
        </div>
      </Field>

      <Field disabled>
        <div className="flex w-full items-center justify-between">
          <Field.Label>Disabled setting</Field.Label>
          <Switch>
            <Switch.Indicator />
          </Switch>
        </div>
      </Field>

      <Field readOnly>
        <div className="flex w-full items-center justify-between">
          <Field.Label>Read-only setting</Field.Label>
          <Switch defaultChecked>
            <Switch.Indicator />
          </Switch>
        </div>
      </Field>
    </div>
  );
}

render(<FieldWithSwitchDemo />);

Controlled


function ControlledSwitchDemo() {
  const [enabled, setEnabled] = useState(true);

  return (
    <div className="grid w-full max-w-90 gap-3">
      <Badge className="w-fit" variant="neutral" appearance="outlined">
        Notifications {enabled ? 'enabled' : 'paused'}
      </Badge>
      <Field>
        <div className="flex w-full items-center justify-between">
          <Field.Label>Flight status notifications</Field.Label>
          <Switch checked={enabled} onChange={setEnabled}>
            <Switch.Indicator />
          </Switch>
        </div>
      </Field>
    </div>
  );
}

render(<ControlledSwitchDemo />);

Sizes


function SizeDemo() {
  return (
    <div className="flex flex-wrap items-center gap-3">
      <Switch aria-label="Extra small switch" size="xsmall">
        <Switch.Indicator />
      </Switch>
      <Switch aria-label="Small switch" size="small">
        <Switch.Indicator />
      </Switch>
      <Switch aria-label="Base switch" size="base" defaultChecked>
        <Switch.Indicator />
      </Switch>
      <Switch aria-label="Large switch" size="large" defaultChecked>
        <Switch.Indicator />
      </Switch>
      <Switch aria-label="Extra large switch" size="xlarge" defaultChecked>
        <Switch.Indicator />
      </Switch>
    </div>
  );
}

render(<SizeDemo />);

Variants


function VariantDemo() {
  return (
    <div className="grid w-full max-w-90 gap-3">
      <Field>
        <div className="flex w-full items-center justify-between">
          <Field.Label>Seat alerts</Field.Label>
          <Switch defaultChecked variant="info">
            <Switch.Indicator />
          </Switch>
        </div>
      </Field>
      <Field>
        <div className="flex w-full items-center justify-between">
          <Field.Label>Auto check-in</Field.Label>
          <Switch defaultChecked variant="success">
            <Switch.Indicator />
          </Switch>
        </div>
      </Field>
    </div>
  );
}

render(<VariantDemo />);

Custom Indicator

Switch.Indicator accepts function children that receive the current checked / disabled / readOnly state, so you can render different content per state. Passing a ReactNode instead replaces the default thumb slot entirely — the consumer becomes responsible for any data-slot="thumb" wrapper they need for theming.


function CustomIndicatorDemo() {
  const SliderThumb = ({ active }) => (
    <span
      aria-hidden="true"
      className="inline-flex h-full w-full items-center justify-center whitespace-nowrap transition-transform duration-200"
    >
      {active ? <CheckIconOutlinedRounded width={20} height={20} /> : <BlockIconOutlinedRounded width={20} height={20} />}
    </span>
  );

  return (
    <Field className="max-w-90">
      <div className="flex w-full items-center justify-between">
        <Field.Label>Auto check-in</Field.Label>
        <Switch defaultChecked variant="success">
          <Switch.Indicator>
            {({ checked }) => (
              <span data-slot="thumb" className="tk-toggle-thumb inline-flex items-center justify-center">
                <SliderThumb active={checked} />
              </span>
            )}
          </Switch.Indicator>
        </Switch>
      </div>
    </Field>
  );
}

render(<CustomIndicatorDemo />);

Render Prop


function RenderPropDemo() {
  return (
    <div>
      <Field>
        <Switch defaultChecked>
          {({ checked }) => (
            <>
              <Field.Label>{checked ? 'Operational alerts active' : 'Operational alerts paused'}</Field.Label>
              <Switch.Indicator />
            </>
          )}
        </Switch>
      </Field>
    </div>
  );
}

render(<RenderPropDemo />);

Form Submission

When name is set, Spar renders a synchronized hidden checkbox input for native form submission.


function SwitchFormDemo() {
  const [saved, setSaved] = useState('Not submitted');

  return (
    <form
      className="grid w-full max-w-90 gap-3"
      onSubmit={(event) => {
        event.preventDefault();
        const data = new FormData(event.currentTarget);
        setSaved(
          [data.get('email') ? 'Email alerts' : null, data.get('sms') ? 'SMS alerts' : null].filter(Boolean).join(', ') || 'No alerts',
        );
      }}
    >
      <Field>
        <div className="flex w-full items-center justify-between">
          <Field.Label>Email alerts</Field.Label>
          <Switch name="email" value="enabled" defaultChecked>
            <Switch.Indicator />
          </Switch>
        </div>
      </Field>
      <Field>
        <div className="flex w-full items-center justify-between">
          <Field.Label>SMS alerts</Field.Label>
          <Switch name="sms" value="enabled">
            <Switch.Indicator />
          </Switch>
        </div>
      </Field>
      <div className="flex flex-wrap items-center justify-end gap-3">
        <Button type="submit">Save</Button>
        <Badge className="w-fit" variant="neutral" appearance="outlined">
          {saved}
        </Badge>
      </div>
    </form>
  );
}

render(<SwitchFormDemo />);

Accessibility

  • Field.Label, Field.Description, and Field.ErrorMessage are wired to the control via stable IDs derived from the Field root (aria-labelledby, aria-describedby, aria-invalid).
  • Switch renders a focusable control with role="switch" and reflects its value through aria-checked.
  • When the switch has no visible label (for example a size sampler), pass aria-label directly on <Switch>.
  • Disabled switches are removed from the tab order. Read-only switches remain focusable and do not change value.
  • When name is provided, Spar renders a synchronized hidden checkbox input for native form submission. name is the form field name, not the accessible label.
KeyBehavior
Enter / SpaceToggle the focused switch.
TabMove focus to the next tabbable UI.

API Reference

Switch

See Spar Switch docs for primitive behavior.

Props

NameTypeDefaultDescription
childrenReact.ReactNode | ((state: SwitchRenderProps) => React.ReactNode)-Compound children for switch anatomy, or a render function exposing Spar state.
sizeSwitchSize'base'Size scale.
variantSwitchVariant'info'Color variant used while checked.
invalidbooleanfalseMarks the switch as visually invalid. Inherited from <Field> automatically; pass this prop only to override.
classNamesPartial<Record<SwitchSlot, string>>-Per-slot class name overrides.
slotPropsPartial<Record<SwitchSlot, React.HTMLAttributes<HTMLElement>>>-Per-slot HTML attribute overrides.
classNamestring-Appends custom classes to the root slot of this part.

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for the root slot.
data-sizeAlwaysReflects the resolved size prop so theme recipes can scope size variants.
data-variantAlwaysReflects the resolved variant prop so theme recipes can scope color variants.
data-state="checked"When checked.Spar checked-state hook.
data-state="unchecked"When unchecked.Spar unchecked-state hook.
data-disableddisabled is true.Spar disabled-state hook.
data-readonlyreadOnly is true.Spar read-only-state hook.
data-requiredrequired is true.Spar required-state hook.
data-invalidinvalid is true.Marks invalid visual state for theme recipes.

Switch.Indicator

See Spar Switch docs for primitive behavior.

Data attributes

AttributeApplied whenPurpose
data-slot="indicator"AlwaysStable selector for the indicator slot.
data-slot="thumb"AlwaysStable selector for the thumb slot.

Type Definitions

NameDefinition
SwitchRenderProps{ checked: boolean; setChecked: (checked: boolean) => void; disabled: boolean; readOnly: boolean; required: boolean; invalid: boolean; isFocused: boolean; isHovered: boolean; isPressed: boolean }
SwitchSize'xlarge' | 'large' | 'base' | 'small' | 'xsmall'
SwitchVariant'info' | 'success'
SwitchSlot'root' | 'indicator' | 'thumb'
SwitchIndicatorRenderProps{ checked: boolean; disabled: boolean; readOnly: boolean }