Permission Wave Grid
An admin flips one switch; SVG wires draw from it to a member grid and avatars unlock wave by wave — showing access granted to everyone at once.
An admin card with a single switch sits on the left (on top in narrow containers), a grid of member avatars sits on the right, and an SVG wire layer connects the two. Turning it on draws the trunk out of the switch's right edge, then branches off to each row in order; the moment a branch reaches its row, that row's avatars pop from grayscale, half-transparent and padlocked to full color with a check badge — wave after wave until everyone has access. Turning it off runs in reverse: wires retract starting from the last row and avatars lock again in turn. The switch state 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 permission-wave-grid.enPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<PermissionWaveGrid/>
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/permission-wave-grid.en.jsonOr, once registries are configured in components.json, install it as @webberui/permission-wave-grid.
Usage
import { PermissionWaveGrid } from "@/components/ui/permission-wave-grid";
const members = [
{ id: "1", name: "王小明" },
{ id: "2", name: "林怡君" },
{ id: "3", name: "陳家豪", initials: "家豪", color: "#0f766e" },
];
<PermissionWaveGrid
members={members}
columns={6}
switchLabel="Let all members view financial reports"
onEnabledChange={(enabled) => console.log(enabled ? "granted" : "revoked")}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
members | PermissionWaveMember[] | — | Member list, placed into the grid in order |
columns | number | 6 | Number of grid columns |
enabled | boolean | — | Controlled switch state; managed internally when omitted (uncontrolled) |
defaultEnabled | boolean | false | Initial switch state in uncontrolled mode |
onEnabledChange | (enabled: boolean) => void | — | Called when the switch is toggled |
adminLabel | string | "Admin" | Admin card title (Chinese registry default: "管理者", "Admin") |
switchLabel | string | "Allow all members access" | Label next to the switch, also its accessible name (Chinese registry default: "允許所有成員存取", "Allow all members access") |
trunkDuration | number | 400 | Trunk wire draw duration (ms) |
waveDelay | number | 160 | Delay between each row's branch (ms) — the wave rhythm |
pulse | boolean | true | Run a light pulse along the wires once access is granted |
accent | string | "#3b82f6" | Accent color, written to --wb-permission-wave-accent |
className | string | — | Merged into the root element |
PermissionWaveMember
| Field | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique id; also used to deterministically assign the default color |
name | string | — | Member name, used for the avatar's aria-label and hover tooltip |
initials | string | — | Avatar initials; derived from the name when omitted (last two chars for CJK names, first letters for Latin names) |
color | string | — | Avatar background; deterministically picked from a mostly-neutral palette by id when omitted |
getMemberInitials(name) is also exported, so you can reuse the same initials rule elsewhere.
How it works
- Wire coordinates are computed from measured DOM positions: the trunk leaves the switch's right edge, runs to a bus just left of the grid, then splits into one orthogonal branch per row with 8px rounded corners. When the container is narrow the layout stacks, and the trunk instead drops from the switch's bottom edge before turning into the grid's left side. A
ResizeObserverrecomputes everything when the layout changes. - Turn-on timeline: the trunk draws over
trunkDuration→ row r's branch starts attrunkDuration + r × waveDelay(300ms) → once it arrives, that row's avatars unlock with a 0.9 → 1.06 → 1 bounce, staggered 30ms within the row. Turning off retracts from the last row back, then the trunk last. - After the wave finishes, if
pulseis on, a light dot travels along each route via CSSoffset-path; its duration reads--wb-permission-wave-pulse(default1600ms). The switch thumb's travel time is--wb-permission-wave-thumb(default150ms). - Rapid toggling retargets the animation from its current state, and the pulse only appears once the latest turn-on has fully played out.
Accessibility
- The switch is a native button with
role="switch"andaria-checked, labelled viaaria-labelledbypointing atswitchLabel; Space and Enter both toggle it, with afocus-visiblering - The member grid is
role="list", and each avatar'saria-labelreads e.g. "Ava Lin: access granted" or "Ava Lin: no access" - When the wave completes, an
aria-live="polite"region announces "Access granted to 24 members" (or that access was revoked) - The SVG wire layer and pulse dots are
aria-hiddenand stay out of the way of assistive tech - When the user's system has reduce motion enabled, wires appear or disappear instantly, all avatars switch together with a short fade, and no pulse runs
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.
Keystroke Card Deck
A stack of cards answered with number keys 1–3; each answered card flies off and the next slides up — keyboard-first, mouse-friendly.