crispmeasured from Attio

Switch

An on/off toggle — Attio's settings switch, measured from the live app.

The toggle Attio uses throughout Settings — a 24×16 pill track that flips from a muted grey to brand blue with a 12px white thumb that slides across. Renders a role="switch" button (a native checkbox's semantics without its box), so label association and keyboard (Space / Enter) come for free.

Preview

Enable daily digest
Sent every morning if any tasks are due.
Mentions
Notify me when someone @mentions me.
Disabled

Installation

bun add crisp
import "crisp/styles.css";
import { Switch } from "crisp";

Usage

Controlled with checked + onCheckedChange, or uncontrolled from defaultChecked:

import { Switch } from "crisp";
import * as React from "react";

function DigestRow() {
  const [on, setOn] = React.useState(true);
  return (
    <label style={{ display: "flex", gap: 12, alignItems: "center" }}>
      <span>Enable daily digest</span>
      <Switch checked={on} onCheckedChange={setOn} />
    </label>
  );
}

Props

PropTypeDefaultDescription
checkedbooleanControlled on/off. Omit for uncontrolled.
defaultCheckedbooleanfalseInitial value when uncontrolled.
onCheckedChange(checked: boolean) => voidFires with the next value on toggle.
disabledbooleanfalseDims and blocks interaction.
aria-labelstringAccessible name when there's no associated <label>.

Any other <button> prop (e.g. id, name) is forwarded.

Notes

  • Semantics. It's a <button role="switch"> with aria-checked — screen readers announce "switch, on/off", and Space/Enter toggle it natively. Give it an aria-label or wrap it in a <label>.
  • Controlled vs uncontrolled. With checked set, the parent owns the value (the switch won't self-update); otherwise it tracks its own state from defaultChecked.
  • Measured. Track 24×16, thumb 12px (2px inset, slides 8px), on-colour #266df0; the background and thumb both animate .2s.

Used across the settings surface — see it in a settings screen soon.

On this page