PluginUI

Step Grid

step-gridControls

Steps run across the grid, lanes run down, and longer patterns use pages. Tap a cell to toggle it. Drag across a row to paint a stroke on or off. Drag vertically on a lit cell to change its velocity, shown by both fill height and brightness; the second cue keeps low velocities visible in a six-pixel fill. Gate controls the note's length within its step. The host supplies the playhead as an absolute step index, so the cursor follows the audio clock rather than a drifting display timer. Past 16 steps, tabs page through the grid; an opt-in follow mode keeps a running sequence on screen. Without callbacks, the component becomes a read-only mini piano roll. The arpeggiator uses that mode to draw its computed line through the same cells a sequencer edits, with ghost cells for previews that are not pattern truth. Layout and hit testing use the same arithmetic in a pure core, so painted strokes match the rectangles they cover.

Bone / Studio
Loading interactive preview…
Planar / Flat
Loading interactive preview…
Midnight / Minimal
Loading interactive preview…
Ember / Vintage
Loading interactive preview…
Ink / Studio
Loading interactive preview…
Mint / Flat
Loading interactive preview…

Install

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

$ pnpm dlx shadcn@latest add @pluginui/core-step-grid

Pulls in

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

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

components.json
{
  "registries": {
    "@pluginui": "https://pluginui.com/r/{name}.json"
  }
}

Usage

Connect the documented props to your application or host.

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 "maruaudio/ui/shared/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 / lanesnumber—The pattern's full size in steps across all pages, rather than only the visible page.
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 | null—Absolute 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) => void—Report a toggle. Present makes the grid editable; absent makes it a read-only drawing with a summary label.
onVelocity(step, lane, velocity) => void—Reports a velocity change from a vertical drag on a lit cell, Shift+arrows on the keyboard, or Shift during a drag for a 4x fine pass. Omit it to make vertical drags paint instead.
pageSizenumber16Steps shown at a time. Tabs appear when the pattern outruns one page.
page / onPageChangenumber / (page) => void—Controlled 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 listed from top to bottom, such as note names or drum names.
onInteraction(event) => void—Fires once on the first gesture of any kind. Use it to resume a suspended AudioContext.
colorstring—Override 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.