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.jsonUsage
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
- Two-prop dispatch.
workgroupSizeandtotalElementsdrive everything — the number of dispatched workgroups is alwaysceil(totalElements / workgroupSize), the grid rendersnumWorkgroups * workgroupSizecells. Controlled and uncontrolled APIs for both. - Out-of-bounds bookkeeping. When
numWorkgroups * workgroupSizeexceedstotalElements, trailing slots are markedoob = true, render red, and surface their count in the stats strip. Clicking an OOB cell narrates the required in-shader guard. - Dispatch wave. Hitting Dispatch schedules one
workgroupStaggerMstimer per workgroup. Each tick flips that workgroup's threads toactive, then todonewhen the next workgroup starts. The last workgroup gets a trailing stagger before settling so the wave reads cleanly. - Phase machine. The root carries
data-phase—idle,dispatching,done, orselectedwhen a cell is chosen. - Cell selection. Each cell is a real
<button role="gridcell">witharia-pressedanddata-status. Selecting toggles the detail strip and the narration line. - 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
| Prop | Type | Default | Description |
|---|---|---|---|
workgroupSize | number | — | Controlled threads-per-workgroup. |
defaultWorkgroupSize | number | 16 | Uncontrolled initial workgroup size. |
onWorkgroupSizeChange | (workgroupSize) => void | — | Fires when the slider moves. |
workgroupSizeRange | readonly [number, number] | [4, 64] | Inclusive slider range. |
workgroupSizeStep | number | 4 | Slider step. |
totalElements | number | — | Controlled element count. |
defaultTotalElements | number | 64 | Uncontrolled initial element count. |
onTotalElementsChange | (totalElements) => void | — | Fires when the slider moves. |
totalElementsRange | readonly [number, number] | [16, 512] | Inclusive slider range. |
totalElementsStep | number | 16 | Slider step. |
selectedCell | WorkgroupGridVizCell | null | — | Controlled selected cell. |
defaultSelectedCell | WorkgroupGridVizCell | null | null | Uncontrolled initial selected cell. |
onSelectedCellChange | (cell) => void | — | Fires when a cell is clicked. |
transition | Transition | SPRINGS.snap | Override the cell colour spring. |
workgroupStaggerMs | number | 120 | Per-workgroup stagger during dispatch. |
hideControls | boolean | false | Hide the slider strip. |
hideActions | boolean | false | Hide the Dispatch / Reset button row. |
hideNarration | boolean | false | Hide the bottom narration. |
narration | ReactNode | auto-generated | Override the narration body. |
dispatchLabel | ReactNode | "Dispatch" | Override the primary button label. |
resetLabel | ReactNode | "Reset" | Override the secondary button label. |
className | string | — | Merged onto the root via cn(). |
Accessibility
- The thread grid is a
role="grid"with anaria-labeldescribing the workgroup × thread shape; every cell is arole="gridcell"button witharia-pressedand a verbosearia-labelcoveringglobal_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 withhtmlFor-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 byModeStrip,ChallengeBtn,FeedbackBadge,ScoreDots,DoneCard, andusePredictRoundsfrom the lesson'sConstructionPrimitivesmodule, plusTogglePill+LabeledSliderchrome, an inlinevar(--color-accent-400)/var(--color-success-400)/var(--color-fail-500)/var(--color-ink-800)palette, and aSPRINGS.snappyreference 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 semanticcb-*tokens (--cb-accent/--cb-success/--cb-error/--cb-bg-muted/--cb-bg-elevated/--cb-fg-*), and swapsSPRINGS.snappyfor the canonicalSPRINGS.snap. The slider strip becomes a pair of token-styled native<input type="range">controls; the actions become inline token-styled<button>s; controlled+uncontrolledworkgroupSize/totalElements/selectedCellplusworkgroupStaggerMs+transitionoverrides let one primitive drive any outer scrubber.forwardRef+cn()+...propsspread were added; lesson-coupledlessonId/ phase callbacks were stripped.