Workgroup Grid Viz

Interactive GPU thread grid with workgroup-by-workgroup dispatch animation. Picks a workgroup size + total element count, animates each workgroup's wave from idle → active → done, and surfaces the global_id = workgroup_id * workgroup_size + local_id mapping by clicking any cell. Out-of-bounds threads stay red so the if (global_id < arrayLength) guard every WGSL / CUDA kernel needs is visible.

Workgroup grid — interactive GPU thread dispatch visualisation.
workgroups: 4threads: 64

64 elements, workgroup_size=16. That means ceil(64/16) = 4 workgroups. Hit Dispatch to see the wave.

Customize
Dispatch
16
64
120 ms

Installation

npx shadcn@latest add https://craftbits.dev/r/workgroup-grid-viz.json

Usage

import { WorkgroupGridViz } from "@craft-bits/viz/workgroup-grid-viz";
 
<WorkgroupGridViz />;

Drive the workgroup size / total elements / selected cell from a parent scrubber (controlled mode):

const [wgSize, setWgSize] = useState(16);
const [total, setTotal] = useState(64);
const [cell, setCell] = useState<WorkgroupGridVizCell | null>(null);
 
<WorkgroupGridViz
  workgroupSize={wgSize}
  onWorkgroupSizeChange={setWgSize}
  totalElements={total}
  onTotalElementsChange={setTotal}
  selectedCell={cell}
  onSelectedCellChange={setCell}
/>;

Embed the grid inside lesson chrome (no built-in sliders, no actions, narration supplied by parent):

<WorkgroupGridViz
  hideControls
  hideActions
  hideNarration
  workgroupSize={32}
  totalElements={200}
/>

Understanding the component

  1. Two-prop dispatch. workgroupSize and totalElements drive everything — the number of dispatched workgroups is always ceil(totalElements / workgroupSize), the grid renders numWorkgroups * workgroupSize cells. Controlled and uncontrolled APIs for both.
  2. Out-of-bounds bookkeeping. When numWorkgroups * workgroupSize exceeds totalElements, trailing slots are marked oob = true, render red, and surface their count in the stats strip. Clicking an OOB cell narrates the required in-shader guard.
  3. Dispatch wave. Hitting Dispatch schedules one workgroupStaggerMs timer per workgroup. Each tick flips that workgroup's threads to active, then to done when the next workgroup starts. The last workgroup gets a trailing stagger before settling so the wave reads cleanly.
  4. Phase machine. The root carries data-phaseidle, dispatching, done, or selected when a cell is chosen.
  5. Cell selection. Each cell is a real <button role="gridcell"> with aria-pressed and data-status. Selecting toggles the detail strip and the narration line.
  6. Reduced motion. Under prefers-reduced-motion: reduce, the per-workgroup stagger collapses to zero — the grid snaps directly to the final state on Dispatch.

Props

PropTypeDefaultDescription
workgroupSizenumberControlled threads-per-workgroup.
defaultWorkgroupSizenumber16Uncontrolled initial workgroup size.
onWorkgroupSizeChange(workgroupSize) => voidFires when the slider moves.
workgroupSizeRangereadonly [number, number][4, 64]Inclusive slider range.
workgroupSizeStepnumber4Slider step.
totalElementsnumberControlled element count.
defaultTotalElementsnumber64Uncontrolled initial element count.
onTotalElementsChange(totalElements) => voidFires when the slider moves.
totalElementsRangereadonly [number, number][16, 512]Inclusive slider range.
totalElementsStepnumber16Slider step.
selectedCellWorkgroupGridVizCell | nullControlled selected cell.
defaultSelectedCellWorkgroupGridVizCell | nullnullUncontrolled initial selected cell.
onSelectedCellChange(cell) => voidFires when a cell is clicked.
transitionTransitionSPRINGS.snapOverride the cell colour spring.
workgroupStaggerMsnumber120Per-workgroup stagger during dispatch.
hideControlsbooleanfalseHide the slider strip.
hideActionsbooleanfalseHide the Dispatch / Reset button row.
hideNarrationbooleanfalseHide the bottom narration.
narrationReactNodeauto-generatedOverride the narration body.
dispatchLabelReactNode"Dispatch"Override the primary button label.
resetLabelReactNode"Reset"Override the secondary button label.
classNamestringMerged onto the root via cn().

Accessibility

  • The thread grid is a role="grid" with an aria-label describing the workgroup × thread shape; every cell is a role="gridcell" button with aria-pressed and a verbose aria-label covering global_id, workgroup_id, local_id, and the out-of-bounds flag.
  • Status is paired with data-status ("idle", "active", "done", "oob") so colour is never the only signal.
  • Sliders are real <input type="range"> controls with htmlFor-linked labels — every native keyboard interaction (arrow keys, Home/End) works.
  • The narration is an aria-live="polite" region.
  • Motion respects prefers-reduced-motion: reduce — the per-workgroup stagger collapses to zero and the grid snaps directly to its final state on Dispatch.

Credits

  • Extracted from: craftingattention (app/src/lessons/primitives/viz/WorkgroupGridViz.tsx). The source bundled Explore + Predict modes in a single widget shell driven by ModeStrip, ChallengeBtn, FeedbackBadge, ScoreDots, DoneCard, and usePredictRounds from the lesson's ConstructionPrimitives module, plus TogglePill + LabeledSlider chrome, an inline var(--color-accent-400) / var(--color-success-400) / var(--color-fail-500) / var(--color-ink-800) palette, and a SPRINGS.snappy reference that doesn't exist on the library's motion module. The viz extract drops the Predict quiz bank (curriculum-specific), strips every lesson-only import, remaps the palette to semantic cb-* tokens (--cb-accent / --cb-success / --cb-error / --cb-bg-muted / --cb-bg-elevated / --cb-fg-*), and swaps SPRINGS.snappy for the canonical SPRINGS.snap. The slider strip becomes a pair of token-styled native <input type="range"> controls; the actions become inline token-styled <button>s; controlled+uncontrolled workgroupSize / totalElements / selectedCell plus workgroupStaggerMs + transition overrides let one primitive drive any outer scrubber. forwardRef + cn() + ...props spread were added; lesson-coupled lessonId / phase callbacks were stripped.