Skip to content
cennge-ui
GitHubv0.3.1, changelog

Switch

An on/off setting that takes effect the moment it flips.

spredo-crypto-screener

Live

Every push to main is built and deployed

A preview address for every pull request

Visitors see a maintenance page

  1. Click a switch or its title; Space or Enter flips the one in focus.
  2. Turn on Maintenance mode: the status above changes at once, with nothing to save.
  3. Deploy on push is disabled meanwhile: it shows off and Tab passes it by.
Live: the component itself, not a picture of it.

Field marks

Import
import { Switch } from "cennge-ui";
Size
1.2 KB gzip for this import, its dependencies aside
Pattern
WAI-ARIA switch (Radix Switch): a <button role="switch">
Keyboard
SpaceEnter
State
Controlled with isSelected and onValueChange, or uncontrolled with defaultSelected
SSR
Rendered on the server in the Next.js check; hydrates without a warning
Forms
Submits "on" under name while on
Tests
Unit tests

When to use#

A Switch is a setting that applies as soon as it changes: notifications, preview deployments, a maintenance page. If the change waits for a Save button, use a Checkbox instead, so nobody expects the flip alone to do it.

Similar#

  • Checkbox is a choice sent with a form, and can be mixed for a list.
  • ToggleGroup switches between named options, such as Code and Preview, rather than on and off.

A labelled switch#

children become a <label> next to the track, so a click on the text flips it too. onValueChange gets the new state as a boolean.

Reviews and merges in cennge-ui email dev@example.com.

basic.tsx
import { useState } from "react";
import { Switch } from "cennge-ui";

export default function NotificationsSwitch() {
  const [isOn, setOn] = useState(true);
  return (
    <div className="flex flex-col gap-2">
      <Switch isSelected={isOn} onValueChange={setOn}>
        Pull request notifications
      </Switch>
      <p className="text-sm text-muted">
        {isOn ? "Reviews and merges in cennge-ui email dev@example.com." : "No email for pull requests."}
      </p>
    </div>
  );
}

With a description#

For a settings list, put the title and its description beside the switch yourself: a <label htmlFor> for the title, and aria-describedby on the switch for the description. Every other prop passes to the switch's button, so id and aria-* land where screen readers look.

Pull requests merge by themselves once every check passes

description.tsx
import { useId } from "react";
import { Switch } from "cennge-ui";

export default function AutoMergeSetting() {
  const id = useId();
  return (
    <div className="flex w-full max-w-md items-start justify-between gap-6">
      <div className="flex flex-col gap-0.5">
        <label htmlFor={id} className="cursor-pointer text-sm font-medium">
          Allow auto-merge
        </label>
        <p id={`${id}-description`} className="text-xs text-muted">
          Pull requests merge by themselves once every check passes
        </p>
      </div>
      <Switch id={id} aria-describedby={`${id}-description`} defaultSelected />
    </div>
  );
}

Colours and sizes#

color paints the track when it is on; primary follows the accent. A disabled switch keeps its state, fades, and leaves the tab order.

colors.tsx
import { Switch, type CenngeColor } from "cennge-ui";

const colors: CenngeColor[] = ["default", "primary", "secondary", "success", "warning", "danger", "info"];

export default function SwitchColors() {
  return (
    <div className="flex flex-col gap-5">
      <div className="flex flex-wrap gap-x-6 gap-y-3">
        {colors.map((color) => (
          <Switch key={color} color={color} defaultSelected>
            {color[0]!.toUpperCase() + color.slice(1)}
          </Switch>
        ))}
      </div>
      <div className="flex flex-wrap items-center gap-x-6 gap-y-3">
        <Switch size="sm" defaultSelected>
          Small
        </Switch>
        <Switch size="md" defaultSelected>
          Medium
        </Switch>
        <Switch size="lg" defaultSelected>
          Large
        </Switch>
        <Switch isDisabled defaultSelected>
          Disabled
        </Switch>
      </div>
    </div>
  );
}

Accessibility#

The switch is a <button role="switch"> with aria-checked, so screen readers announce it as a switch that is on or off. Without visible text, give it an aria-label. The thumb's movement follows the reduced-motion setting: with it, the thumb jumps rather than slides.

Props#

PropTypeDefault
children
React.ReactNodeLabel next to the switch. Without it, pass aria-label.
–
isSelected
boolean
–
defaultSelected
boolean
–
onValueChange
(isSelected: boolean) => void
–
isDisabled
boolean
–
color
"default""primary""secondary""success""warning""danger""info"
"primary"
size
"sm""md""lg"
"md"
name
stringForm field name; the value is submitted as "on".
–

Also takes every attribute of <button>, except color, onChange, value, defaultValue, which mean something else here.