Switch

Developer Experience (DX)

How developers use the Switch, with one consistent, typed API that works with your forms and leaves its state to you or the browser.

Next, let's look at how developers use the Switch. It has one API, with the same names as every other component, it's typed end to end, and it works with your forms however you build them.

import { useState } from "react";
import { Switch } from "@/components/Switch";

export function Notifications() {
  const [on, setOn] = useState(true);

  return (
    <>
      <label htmlFor="notifications">Notifications</label>
      <Switch id="notifications" checked={on} onChange={(event) => setOn(event.target.checked)} />
    </>
  );
}

Consistent

Once you've learned the Input's API, you already know most of the Switch's. These names mean the same thing everywhere in Open Components:

NameKindWhat it means
sizePropxs, sm, md, lg or xl, with the same names everywhere
disabledPropThe component can't be used
readonlyPropPeople can see and focus the value, but not change it
checkedPropWhether it's on, which you bind the way your framework binds a checkbox's

We recommend size="md" as the default. The Switch also leaves a few props out, on purpose:

  • There's no label, hint or error. Each of them is an element of its own, linked by ID, which the Field on our Roadmap will render, so the Switch stays one control that fits any layout, like a settings list with the label at the start of the row.
  • There's no color. A switch has no intent of its own, so on is always the primary colour, and its error state comes from aria-invalid.
  • There's no loading. The switch moves as soon as it's flipped, and a status message says when the change is saved, so there's nothing to wait for on the switch itself.
  • There's no indeterminate. A switch is on or off, so a group with some options on and some off is better as checkboxes under a parent checkbox, which has a mixed state.

Type-safe

Props and events are typed, so your editor can autocomplete them and your type checker (tsc, vue-tsc, svelte-check, ngc or astro check) catches mistakes like size="medium", which isn't one of the Switch's sizes.

PropTypeDefaultDescription
size"xs" | "sm" | "md" | "lg" | "xl""md"How big the switch is
disabledbooleanfalseBlocks focus, flipping and submission
readonlybooleanfalseBlocks flipping, while it keeps focus and submission
checkedbooleanfalseWhether it's on, when you bind it. Without it, the switch keeps its own
EventPayloadDescription
changeEventFires when people flip the switch

It comes straight from the <input>, like every other native event, such as focus and blur, and a binding to checked listens to it for you. The Switch has no slots, since its label sits outside it.

We also export SwitchProps, which comes in handy for wrappers and data-driven settings:

import type { SwitchProps } from "@/components/Switch";

const settings: (SwitchProps & { name: string; label: string })[] = [
  { name: "wifi", label: "Wi-Fi", checked: true },
  { name: "bluetooth", label: "Bluetooth" },
];

Composable

The Switch renders a few elements, so it splits what you pass between them. Classes and styles go on the root, so a class can set its variables. Everything else lands on the <input>, including id, name, value, ARIA attributes and event listeners. A ref to the Switch reaches the <input> too, since that's what form libraries and focus() need.

Forms

The <input> is a real checkbox, so the Switch works with your forms however you build them. While it's on, its form sends its value, which is on unless you set one, under its name, and while it's off, the form leaves it out, like any checkbox. Form libraries register the <input> itself, through its ref or its attributes and listeners, which is why the Switch puts them all on the <input>.

Keep in mind that a switch still applies right away inside a form. If the form only saves its settings when people press its submit button, use checkboxes instead, as under Apply it right away.

With other components

  • The Field, on our Roadmap, will render the label, hint and error message around a Switch, give it an id, and set its aria-describedby and aria-invalid for you.
  • The Checkbox is for choices that are sent with a form. It takes the same size names and states as the Switch, and adds an indeterminate state.
  • A toggle button is for an action that stays pressed, like Bold or Mute in a toolbar, rather than a setting.
  • The Input and the Button take the same size names, so a row of form controls can share one.

Controllable

The Switch doesn't keep any state you can't reach. Whether it's on is yours to bind, or to leave to the browser, and every other state is a prop or an attribute that you control:

StateHow you control it
Checkedchecked, bound to your state, or left to the browser
Disableddisabled
Read-onlyreadonly
Invalidaria-invalid, set while its error message shows (see Invalid)
FocusA ref to its <input>, and focus()

DX Checklist

RuleLevelScopeRequirementBasisCheck
switch/shared-vocabularyMustComponentIt uses the shared names: size, disabled, readonly and checkedOpen ComponentsReview
switch/typedMustComponentProps and events are typed, with unions rather than stringOpen ComponentsType check
switch/control-attributesMustComponentAttributes, listeners and refs land on the <input>, while classes and styles land on the rootOpen ComponentsUnit test
switch/form-controlMustComponentIts form sends its value under its name while it's on, whether checked is bound or notHTMLUnit test
switch/controllableShouldComponentWhether it's on can be bound or left to the browser, and every other state is a prop or an attributeOpen ComponentsUnit test

FAQ

Copyright © 2026