Step Grid

step-gridControls

Steps across, lanes down, a page at a time. Tap a cell to toggle it, drag across a row to paint a whole stroke on or off, drag up or down on a lit cell to ride its velocity — velocity is drawn as fill height and brightness both, because a six-pixel fill needs the second cue, and gate is the fill's width: the note's length inside its own step, drawn as exactly that. The playhead is a prop, an absolute step index, the same way lfo-visualizer takes phase: the host owns the clock, so the cursor is wherever the audio actually is rather than wherever a display timer drifted to. Past 16 steps the grid pages like the hardware it is quoting, with tabs and an opt-in follow mode that keeps a running sequence on screen. Leave the callbacks off and it becomes a read-only mini piano-roll — which is how the arpeggiator draws its computed line through the same cells a sequencer edits, with ghost cells for a preview that is not pattern truth. Layout and hit-testing are the same arithmetic in a pure core, so a paint stroke cannot disagree with the rectangles it paints.

Run
ON

Tap a cell to toggle it, drag across a row to paint a stroke, drag up or down on a lit cell to ride its velocity (Shift is fine). Velocity is fill height and brightness both; gate is the fill's width. The playhead is a prop — this demo owns the clock, the grid just draws it — and the page follows it.

Install

Copies the source into your project. Anything it depends on comes with it.

$ npx shadcn add @syntho-ui/step-grid

Pulls in

step-patternuse-surface-pointeruse-first-interactioncontrol-theme

First time? Add the registry to your components.json once:

components.json
{
  "registries": {
    "@syntho-ui": "https://engine.syntho.app/r/{name}.json"
  }
}

Usage

Controlled or uncontrolled, whichever suits the surrounding state.

example.tsx
import { StepGrid } from "@/components/ui/step-grid"

// An editable 16-step, 4-lane pattern. The host owns the cells and the clock.
<StepGrid
  steps={16}
  lanes={4}
  cells={cells}
  laneLabels={["hat", "snare", "clap", "kick"]}
  playhead={currentStep}
  onToggle={(step, lane, active) => setCell(step, lane, active)}
  onVelocity={(step, lane, velocity) => setVelocity(step, lane, velocity)}
/>

// Read-only: an arpeggiator's computed line as a mini piano-roll.
import { sequenceGrid, stepNoteName } from "@/lib/step-pattern"
const model = sequenceGrid(heldNotes, { order: "updown", octaves: 2 })
<StepGrid
  steps={model.steps}
  lanes={model.laneNotes.length}
  cells={model.cells.map((c) => ({ step: c.step, lane: c.lane, gate: 0.6 }))}
  laneLabels={model.laneNotes.map(stepNoteName)}
  playhead={activeStep}
/>

Props

PropTypeDefaultDescription
steps / lanesnumberThe pattern's full size — steps counts across every page, not the visible one.
cellsStepGridCellState[]The lit cells, each addressed by absolute step and lane, carrying velocity (0..1), gate (0..1), an optional ghost flag for computed previews, and an optional per-cell color. Everything absent is an empty cell.
playheadnumber | nullAbsolute step index the host's clock is on. Drawn as a lit column when it falls on the visible page; null draws no cursor.
onToggle(step, lane, active) => voidReport a toggle. Present makes the grid editable; absent makes it a read-only drawing with a summary label.
onVelocity(step, lane, velocity) => voidReport a velocity ride — vertical drag on a lit cell, Shift+arrows on the keyboard, Shift mid-drag for a 4x fine pass. Omit it and vertical drags paint instead.
pageSizenumber16Steps shown at a time. Tabs appear when the pattern outruns one page.
page / onPageChangenumber / (page) => voidControlled paging. Omit page to let the grid keep its own.
followPlayheadbooleanfalseFlip pages to keep the playhead on screen. Off by default so paging around a running sequence does not fight the cursor.
groupSizenumber4Columns per beat-group: alternating groups carry a faint tint so beats stay countable. 0 disables.
laneLabelsstring[]Row captions, top to bottom — note names, drum names.
onInteraction(event) => voidFired once, on the first gesture of any kind — the place to resume a suspended AudioContext.
colorstringOverride the accent colour for this instance.
heightnumber160Grid surface height in pixels.

Interaction

  • Drag with pointer capture, so the gesture keeps tracking past the edge of the control.
  • Shift while dragging for a fine pass; release it mid-gesture and coarse tracking resumes.
  • Double-click resets to defaultValue.
  • Arrow keys step normally, Shift+arrow steps finely, PageUp/PageDown jump by ten, Home/End go to the extremes.
  • Proper slider and switch roles with live aria-valuetext, so a screen reader announces the formatted value rather than the raw number.
  • The scroll wheel adjusts the value while the pointer is over the control; stepped ranges move at least one declared step per notch by default.