Skip to content
design
Components

Switch

An on/off control with a thumb that moves along a track.

On this page
Sounds when SV Althea leaves the 45 m swing radius.
Plays through the cabin speakers from 22:00 to 06:00, not only in the cockpit.
Anneke hears every alarm in the aft cabin, not only the watch leader.
Off: there is no mobile signal at this anchorage, so messages would wait until the next harbor.
Always on at sea; the skipper set it so. Ask Anneke to change it in harbor.
The alarms at anchor: on and off, one saving as it flips, one disabled until another is on, one locked on.
Code
import { Label } from "@bosunkit/components/label";
import { SaveChip, type SaveState } from "@bosunkit/components/setting-control";
import { Switch } from "@bosunkit/components/switch";
import { Anchor, BellRing, Moon, Send, Waves, type LucideIcon } from "lucide-react";
import {
  useEffect,
  useId,
  useRef,
  useState,
  type CSSProperties,
  type Dispatch,
  type ReactElement,
  type ReactNode,
  type RefObject,
  type SetStateAction,
} from "react";

export const description: string =
  "The alarms at anchor: a switch that saves as it flips, one that is disabled while the anchor alarm is off, " +
  "one held on by the skipper, and ones that start on and off, each named by its label.";

const SAVE_MILLISECONDS: number = 600;
const SAVED_MILLISECONDS: number = 1800;

const ROW: CSSProperties = {
  display: "grid",
  gridTemplateColumns: "18px minmax(0, 1fr) auto",
  alignItems: "center",
  gap: "4px var(--space-md)",
  padding: "10px 0",
  borderBottom: "1px solid var(--border)",
};
const NOTE: CSSProperties = { gridColumn: 2, fontSize: 12, color: "var(--text-dim)" };

function Setting({
  icon: Icon,
  id,
  name,
  note,
  noteId,
  children,
}: {
  icon: LucideIcon;
  id: string;
  name: string;
  note: string;
  noteId?: string | undefined;
  children: ReactNode;
}): ReactElement {
  return (
    <div style={ROW}>
      <Icon size={16} aria-hidden="true" style={{ color: "var(--text-dim)" }} />
      <Label htmlFor={id} style={{ marginBottom: 0, cursor: "pointer" }}>
        {name}
      </Label>
      <div style={{ display: "flex", alignItems: "center", gap: "var(--space-sm)", gridRow: "span 2" }}>{children}</div>
      <span id={noteId} style={NOTE}>
        {note}
      </span>
    </div>
  );
}

export default function Showcase(): ReactElement {
  const id: string = useId();
  const [anchorAlarm, setAnchorAlarm]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
  const [atNight, setAtNight]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
  const [save, setSave]: [SaveState, Dispatch<SetStateAction<SaveState>>] = useState<SaveState>("idle");
  const timer: RefObject<number | undefined> = useRef<number | undefined>(undefined);

  useEffect((): (() => void) => {
    return (): void => {
      window.clearTimeout(timer.current);
    };
  }, []);

  function flipAnchorAlarm(checked: boolean): void {
    setAnchorAlarm(checked);
    setSave("saving");
    window.clearTimeout(timer.current);
    timer.current = window.setTimeout((): void => {
      setSave("saved");
      timer.current = window.setTimeout((): void => {
        setSave("idle");
      }, SAVED_MILLISECONDS);
    }, SAVE_MILLISECONDS);
  }

  return (
    <div style={{ width: "100%", maxWidth: 560 }}>
      <Setting
        icon={Anchor}
        id={`${id}-anchor`}
        name="Anchor alarm"
        note={anchorAlarm ? "Sounds when SV Althea leaves the 45 m swing radius." : "The anchor is not watched."}
      >
        <SaveChip state={save} />
        <Switch id={`${id}-anchor`} checked={anchorAlarm} onCheckedChange={flipAnchorAlarm} />
      </Setting>
      <Setting
        icon={Moon}
        id={`${id}-night`}
        name="Sound at night"
        noteId={`${id}-night-note`}
        note={
          anchorAlarm
            ? "Plays through the cabin speakers from 22:00 to 06:00, not only in the cockpit."
            : "Turn the anchor alarm on to choose where it sounds at night."
        }
      >
        <Switch
          id={`${id}-night`}
          checked={anchorAlarm && atNight}
          disabled={!anchorAlarm}
          aria-describedby={`${id}-night-note`}
          onCheckedChange={setAtNight}
        />
      </Setting>
      <Setting
        icon={BellRing}
        id={`${id}-skipper`}
        name="Wake the skipper too"
        note="Anneke hears every alarm in the aft cabin, not only the watch leader."
      >
        <Switch id={`${id}-skipper`} defaultChecked />
      </Setting>
      <Setting
        icon={Send}
        id={`${id}-telegram`}
        name="Send alarms to Telegram"
        note="Off: there is no mobile signal at this anchorage, so messages would wait until the next harbor."
      >
        <Switch id={`${id}-telegram`} name="telegram" value="on" />
      </Setting>
      <Setting
        icon={Waves}
        id={`${id}-bilge`}
        name="Bilge water high"
        noteId={`${id}-bilge-note`}
        note="Always on at sea; the skipper set it so. Ask Anneke to change it in harbor."
      >
        <Switch id={`${id}-bilge`} checked disabled aria-describedby={`${id}-bilge-note`} />
      </Setting>
    </div>
  );
}

A switch is an on/off control for a setting that takes effect when it is flipped. Its state is data-state (checked or unchecked) and aria-checked. It has no text of its own, so give it an accessible name with aria-label or a label that points at it. It takes the props of Radix's switch.

Examples

Off

Off, the thumb rests at the start of the track. A tap turns it on.
Code
import { Switch } from "@bosunkit/components/switch";
import type { ReactElement } from "react";

export const description: string = "Off, the thumb rests at the start of the track. A tap turns it on.";

export default function Off(): ReactElement {
  return <Switch aria-label="Anchor alarm" />;
}

On

On, with defaultChecked, the thumb sits at the end of the track. A tap turns it off.
Code
import { Switch } from "@bosunkit/components/switch";
import type { ReactElement } from "react";

export const description: string = "On, with defaultChecked, the thumb sits at the end of the track. A tap turns it off.";

export default function On(): ReactElement {
  return <Switch aria-label="Anchor alarm" defaultChecked />;
}

Disabled

Disabled, the switch is dimmed and takes no tap.
Code
import { Switch } from "@bosunkit/components/switch";
import type { ReactElement } from "react";

export const description: string = "Disabled, the switch is dimmed and takes no tap.";

export default function Disabled(): ReactElement {
  return <Switch aria-label="Anchor alarm" disabled />;
}

A stable name and a confirmed state

Name the setting, such as “Automatic updates,” and keep that label stable as the state changes. checked expresses the value; onCheckedChange requests the next one. An uncontrolled switch with defaultChecked only keeps local UI state and does not save a vessel setting.

For an asynchronous setting, use SettingToggle or show pending and failure feedback beside the switch. Keep the confirmed state authoritative and explain an unavailable choice without relying on a hover hint. A switch has two states; use a select or segmented control for Auto plus explicit modes. Test Space from keyboard focus and the entire labeled target on touch.

Props

PropTypeDefaultDescription
classNamestring | ((state: State) => string | undefined) | undefined–CSS class applied to the element, or a function that returns a class based on the component's state.
renderReact.ReactElement | ComponentRenderFn<RenderFunctionProps, State> | undefined–Allows you to replace the component's HTML element with a different tag, or compose it with another component. Accepts a ReactElement or a function that returns the element to render.
styleReact.CSSProperties | ((state: State) => React.CSSProperties | undefined) | undefined–Style applied to the element, or a function that returns a style object based on the component's state.
nativeButtonboolean | undefined–Whether the component renders a native <button> element when replacing it via the render prop. Set to true if the rendered element is a native button.
idstring | undefined–The id of the hidden input element. When nativeButton is true, the id is applied to the root element.
checkedboolean | undefined–Whether the switch is currently active. To render an uncontrolled switch, use the defaultChecked prop instead.
defaultCheckedboolean | undefined–Whether the switch is initially active. To render a controlled switch, use the checked prop instead.
disabledboolean | undefined–Whether the component should ignore user interaction.
inputRefReact.Ref<HTMLInputElement> | undefined–A ref to access the hidden <input> element.
namestring | undefined–Identifies the field when a form is submitted.
formstring | undefined–Identifies the form that owns the hidden input. Useful when the switch is rendered outside the form.
onCheckedChange((checked: boolean, eventDetails: SwitchRoot.ChangeEventDetails) => void) | undefined–Event handler called when the switch is activated or deactivated.
readOnlyboolean | undefined–Whether the user should be unable to activate or deactivate the switch.
requiredboolean | undefined–Whether the user must activate the switch before submitting a form.
valuestring | undefined–The value submitted with the form when the switch is on. By default, switch submits the "on" value, matching native checkbox behavior.
uncheckedValuestring | undefined–The value submitted with the form when the switch is off. By default, unchecked switches do not submit any value, matching native checkbox behavior.

Also takes every attribute of Primitive.Root.Props.

Do
Name a switch by the setting it turns on. On then reads as the setting working.
Don't
Don't name a switch by turning something off. Disable anchor alarm switched on means the alarm is off, and the crew must work out the double negative.
  • Checkbox: independent choices collected as part of a form.

  • Label: the caption that names a switch.

  • Accessibility: the target sizes and the accessible name a control needs.