Drum Lane

drum-laneViewers

The rhythm sibling of the piano roll. Named lanes — Kick, Snare, Hat — run down the left with a beat grid across, and every hit is drawn at its exact onset as a bright dot, so a hit that is pushed or swung visibly sits off its gridline instead of snapping to the middle of a step. That is the whole point: the step-dot preview it replaces quantised the picture, so straight and swung sixteenths looked identical; here the sixteenth grid stays legible and the marker sits where the hit actually is. Velocity is the marker's size and opacity so a ghost note reads as a ghost note, accents wear a ring, and a sustained hit — an open hat, a ride — grows a tail. It reuses the piano roll's whole machinery through the shared geometry core: the same beat axis, the same host-driven playhead that draws nothing when the transport is stopped, the same active-hit highlight that lights whatever is sounding, the same --uikit-* theming that follows a host repainting per mode. It changes only what a drum view needs — taller rows, text labels (regular type, ellipsized and clipped to the gutter so a long name never spills onto the grid) instead of a keyboard, hits placed by lane instead of pitch.

KickSnareHat

Kick, snare and hats over one bar. Push the Swing slider and watch the offbeat hats slide to the right of their sixteenth line — the same movement a step-dot grid hides, because it snaps every hit to the middle of its cell. Each hit is a dot at its real onset; velocity sets its size, so the ghosted offbeat hats read as quieter. The head sweeps and lights whatever hit it is on. Same playhead, active-state and theming as the piano roll — a drum view is that roll with named lanes instead of pitches.

Install

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

$ npx shadcn add @syntho-ui/drum-lane

Pulls in

piano-roll-geometrycontrol-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 { DrumLane } from "@/components/ui/drum-lane"

const lanes = [
  { id: "kick", label: "Kick" },
  { id: "snare", label: "Snare" },
  { id: "hat", label: "Hat" },
]

// The host owns the clock: playhead is a beat position, null hides the cursor.
<DrumLane
  lanes={lanes}
  hits={hits}              // { id, lane, startBeats, durationBeats?, velocity?, accent? }
  bars={2}
  playhead={playing ? loopBeats : null}
  onHitActivate={(hit) => audition(hit.lane)}
/>

Props

PropTypeDefaultDescription
lanesDrumLaneRow[]The lanes top to bottom, each with a stable id (hits reference it), a label for the gutter, and an optional per-lane color.
hitsDrumHit[]The hits, each placed by lane and startBeats — never by pitch — with an optional durationBeats (a tail; 0 is a pure onset), velocity (0..127, drawn as marker size and opacity), and an accent flag that adds a ring. A hit whose lane matches no row is dropped.
bars / beatsPerBarnumberThe pattern's length in bars, and the beats a bar counts (default 4).
playheadnumber | nullAbsolute beat the host's clock is on. Drawn as a lit head with a bar.beat.sixteenth readout; null draws no cursor, so a stopped transport shows nothing pinned at zero.
highlightActiveHitsbooleantrueLight up the hit the playhead is on, for live feedback on what is sounding. No effect while the playhead is null.
subdivisionsnumber4Grid lines per beat. 4 draws the sixteenth grid, which is what makes an off-grid (swung, pushed) hit read as off-grid. Bar and beat lines are always drawn and heaviest.
onSelectionChange / selectedIds(ids) => void / string[]Selection. Present makes hits selectable (click to select, Shift-click to extend); absent is read-only.
onHitActivate(hit: DrumHit) => voidFired on a plain click / Enter / Space on a hit — the hook to audition it.
scheme"inherit" | "light" | "dark""inherit"Colour scheme, scoped to the component so a light lane can sit inside a dark module. The accent is left to inherit so the host's team colour lights the hits.
rowHeightnumber30Row height in pixels — drum rows are tall on purpose so the hits are easy to read.
gutterWidthnumber60Width of the lane-label gutter, in layout px.
colorstringOverride the accent for this instance. Otherwise the lane reads --uikit-accent, so it follows a themed host automatically.

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.