Developer Experience (DX)
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:
| Name | Kind | What it means |
|---|---|---|
size | Prop | xs, sm, md, lg or xl, with the same names everywhere |
disabled | Prop | The component can't be used |
readonly | Prop | People can see and focus the value, but not change it |
checked | Prop | Whether 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,hintorerror. 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 fromaria-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.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | How big the switch is |
disabled | boolean | false | Blocks focus, flipping and submission |
readonly | boolean | false | Blocks flipping, while it keeps focus and submission |
checked | boolean | false | Whether it's on, when you bind it. Without it, the switch keeps its own |
| Event | Payload | Description |
|---|---|---|
change | Event | Fires 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 itsaria-describedbyandaria-invalidfor you. - The Checkbox is for choices that are sent with a form. It takes the same
sizenames 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
sizenames, 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:
| State | How you control it |
|---|---|
| Checked | checked, bound to your state, or left to the browser |
| Disabled | disabled |
| Read-only | readonly |
| Invalid | aria-invalid, set while its error message shows (see Invalid) |
| Focus | A ref to its <input>, and focus() |
DX Checklist
| Rule | Level | Scope | Requirement | Basis | Check |
|---|---|---|---|---|---|
switch/shared-vocabulary | Must | Component | It uses the shared names: size, disabled, readonly and checked | Open Components | Review |
switch/typed | Must | Component | Props and events are typed, with unions rather than string | Open Components | Type check |
switch/control-attributes | Must | Component | Attributes, listeners and refs land on the <input>, while classes and styles land on the root | Open Components | Unit test |
switch/form-control | Must | Component | Its form sends its value under its name while it's on, whether checked is bound or not | HTML | Unit test |
switch/controllable | Should | Component | Whether it's on can be bound or left to the browser, and every other state is a prop or an attribute | Open Components | Unit test |
FAQ
Space, and its form sends it. Only applying the setting right away needs a script, so for a settings page that should work without one, put the switches in a form with a submit button as well. A read-only switch is the exception: the script is what stops it from flipping, so until it loads, people can flip it and its form sends the new value, which your server shouldn't trust.