Interaction Choreography

Master Switch Cascade

A master switch cascade React component: flip one master switch and the child switches in a grid follow in a distance-based ripple, each cell flashing a ring as it flips; children stay individually toggleable and the master reflects on, off or mixed.

Gather a whole group of settings under a single master switch. When the master flips, the component measures the distance from each child cell's centre to the origin (the master switch, the grid's top-left corner or the grid's centre) and schedules proportional delays, so the child switches flip in a ripple from nearest to farthest, each cell flashing an accent ring at the moment it changes. Children can also be toggled one by one, and the master always reflects all on, all off or partial (shown with a "—" in the thumb). Disabled items stay out of the cascade. The switch itself (track + thumb) is implemented inside the component with no dependency on other components, and the value supports controlled and uncontrolled modes.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add master-switch-cascade.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

600
3
Active edited usageDefault props
<MasterSwitchCascade/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/master-switch-cascade.en.json

Or, once registries are configured in components.json, install it as @webberui/master-switch-cascade.

Usage

import { Package, CreditCard, Truck, MessageSquare } from "lucide-react";
import { MasterSwitchCascade } from "@/components/ui/master-switch-cascade";

const items = [
  { id: "orders", label: "Order updates", icon: <Package /> },
  { id: "payments", label: "Payment reminders", icon: <CreditCard /> },
  { id: "shipping", label: "Shipping alerts", icon: <Truck /> },
  { id: "reviews", label: "Review replies", icon: <MessageSquare /> },
];

export function NotificationSettings() {
  const [value, setValue] = React.useState<Record<string, boolean>>({
    orders: true,
  });
  return (
    <MasterSwitchCascade
      items={items}
      value={value}
      onChange={setValue}
      masterLabel="Enable all"
      origin="master"
      spread={600}
      columns={4}
    />
  );
}

Props

PropTypeDefaultDescription
itemsMasterSwitchItem[]—The child switches
valueRecord<string, boolean>—Controlled value: switch states keyed by item id; when omitted the component manages it internally (uncontrolled)
defaultValueRecord<string, boolean>{}Initial state in uncontrolled mode; missing ids count as off
onChange(value: Record<string, boolean>) => void—Called whenever a child switch changes; during a cascade it fires once per child as each one flips
masterLabelstring"Enable all"Title of the master row, also the master switch's accessible name (the Chinese version defaults to "全部啟用", "enable all")
origin"master" | "top-left" | "center""master"Ripple origin: the master switch's centre, the grid's top-left corner or the grid's centre
spreadnumber600Total cascade span (ms): the farthest child flips after this delay
columnsnumber4Number of grid columns
accentstring"#3b82f6"Accent colour, written to --wb-master-switch-accent
classNamestring—Forwarded to the outermost container

MasterSwitchItem

FieldTypeDefaultDescription
idstring—Unique id, also the key in the value object
labelstring—Display label, also the switch's accessible name
iconReactNode—Icon shown in the cell's top-left corner (lucide-react icons recommended)
disabledbooleanfalseDisabled: cannot be toggled and does not take part in the master cascade

How it works

  • Delay formula: each cell's delay = its centre's distance to the origin ÷ the farthest distance among all participating cells × spread. The farthest distance is measured over every non-disabled cell, so a given cell always gets the same delay and the rhythm stays stable.
  • Toggle rule: flipping the master while it is all on turns everything off; flipping it while all off or partial turns everything on. Only children whose state actually needs to change are queued and flash a ring.
  • Queue handling: delayed child flips are scheduled with a setTimeout queue; flipping the master again clears the queue first and restarts from the current state, and unmounting clears it too. Toggling a cell by hand mid-cascade cancels that cell's pending flip — the manual choice wins.
  • While a cascade is running, the master shows the target state directly instead of flashing to partial midway.
  • The tri-state only counts non-disabled items: a disabled item's state does not affect the master.
  • CSS variables: --wb-master-switch-duration (thumb travel and track colour, default 150ms), --wb-master-switch-ring (ring duration, default 400ms), --wb-master-switch-accent (accent colour, written from the accent prop).

Accessibility

  • Each child is a type="button" with role="switch" and aria-checked for its state; its accessible name comes from the label inside the cell, and disabled items set disabled
  • The master needs to express a partial state, and role="switch" does not support aria-checked="mixed", so the master always uses role="checkbox" with aria-checked of true / false / mixed, named via aria-labelledby pointing at the master title
  • The child grid is a role="group" named by the same master title
  • Every switch is a native button, so Space and Enter both toggle it, with a focus-visible ring
  • When a cascade finishes, a role="status", aria-live="polite" region announces the result, such as "12 enabled" or "11 turned off"
  • When the user has reduce motion enabled at the system level, all children flip at once with no ring, and the thumb transition is skipped