Interaction Choreography

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.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.

6
160
400
Active edited usageDefault props
<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.json

Or, 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

PropTypeDefaultDescription
membersPermissionWaveMember[]—Member list, placed into the grid in order
columnsnumber6Number of grid columns
enabledboolean—Controlled switch state; managed internally when omitted (uncontrolled)
defaultEnabledbooleanfalseInitial switch state in uncontrolled mode
onEnabledChange(enabled: boolean) => void—Called when the switch is toggled
adminLabelstring"Admin"Admin card title (Chinese registry default: "管理者", "Admin")
switchLabelstring"Allow all members access"Label next to the switch, also its accessible name (Chinese registry default: "允許所有成員存取", "Allow all members access")
trunkDurationnumber400Trunk wire draw duration (ms)
waveDelaynumber160Delay between each row's branch (ms) — the wave rhythm
pulsebooleantrueRun a light pulse along the wires once access is granted
accentstring"#3b82f6"Accent color, written to --wb-permission-wave-accent
classNamestring—Merged into the root element

PermissionWaveMember

FieldTypeDefaultDescription
idstring—Unique id; also used to deterministically assign the default color
namestring—Member name, used for the avatar's aria-label and hover tooltip
initialsstring—Avatar initials; derived from the name when omitted (last two chars for CJK names, first letters for Latin names)
colorstring—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 ResizeObserver recomputes everything when the layout changes.
  • Turn-on timeline: the trunk draws over trunkDuration → row r's branch starts at trunkDuration + 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 pulse is on, a light dot travels along each route via CSS offset-path; its duration reads --wb-permission-wave-pulse (default 1600ms). The switch thumb's travel time is --wb-permission-wave-thumb (default 150ms).
  • 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" and aria-checked, labelled via aria-labelledby pointing at switchLabel; Space and Enter both toggle it, with a focus-visible ring
  • The member grid is role="list", and each avatar's aria-label reads 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-hidden and 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