Super-AI-Components

Track List

Music library rows

ArtworkTrackTagsPreviewBPMKey
Not set
Nova Kane
synthwaveinstrumental
124F minor
Not set
Ilya Sound
ambientloop
92C major
Not set
Nova Kane
Not set
124Not set
Not set
Not set
Not setNot set

What it is

A table of music library rows: cover art, title and artist, tags, a row-height waveform you can audition in place, tempo and musical key. Every row is a track; every column is something you would compare across tracks.

Why it matters

Tempo and key are the two fields a music library is actually sorted and filtered by, and a generic asset list cannot express either — which is why this exists as its own component instead of being folded into `asset-library`. Once those fields are columns rather than free text, two takes line up digit for digit and the comparison costs nothing. That is also why this is a table while the rest of the library family is grid-shaped: a card grid looks better and hides the numbers you opened the library to compare. The inline waveform follows the same logic — it is an audition affordance, not artwork. Playing a row must leave you exactly where you were, because comparing three takes should not cost three round trips through a detail view. And because a separated stem has no cover art and a spoken clip has no key, absence is a first-class value here: a missing field is stated, never filled in with a zero that reads as a measurement.

Anatomy

  1. 1track-listThe table wrapper.
  2. 2track-list-rowOne track. Carries data-state — playing or idle.
  3. 3track-list-artworkCover art cell. Your own decorative node, or a stated absence.
  4. 4track-list-titleThe track title. A button when onSelect is given, otherwise text.
  5. 5track-list-artistSecondary line under the title.
  6. 6track-list-tagsFree-text tags. Not where tempo or key belong.
  7. 7track-list-playThe audition control, named for its track. Rendered only when onPlayToggle is supplied.
  8. 8track-list-waveformRow-height preview strip, decorative. data-peaks="none" when the track has not been analysed.
  9. 9track-list-waveform-barOne bar per supplied peak; bars behind the playhead fill while the row is sounding.
  10. 10track-list-bpmTempo. Right-aligned and tabular so columns compare at a glance.
  11. 11track-list-keyMusical key, as free text — notation conventions differ.
  12. 12track-list-unsetA deliberate absence: an em-dash hidden from assistive tech, paired with the words "Not set".

How to use it

Reach for this over a generic asset library the moment tempo or key matter — those two fields are the whole reason it is a separate component, and a list that cannot sort by them is an asset library with music in it. Keep it a table: the columns exist to be compared, and turning it into a card grid throws that away. Playback is yours: the component holds no audio element and no timer, so hand it `playingId` and a `progress` percentage from whatever is actually sounding and handle `onPlayToggle` by starting or stopping in place. Sorting is yours too — pass `tracks` in the order you want them, driven by whichever column the user picked. Give `onSelect` only if opening a track is a real destination; it renders as a control on the title, deliberately separate from play.

Do

  • Give tempo and key their own columns, so two takes can be compared without opening either.

    ArtworkTrackTagsPreviewBPMKey
    Not set
    Midnight DriveNova Kane
    synthwave
    124F minor
    Not set
    Paper LanternsIlya Sound
    ambient
    92C major
  • Let a row with no cover art, no tags or no key say so — absence is a value.

    ArtworkTrackTagsPreviewBPMKey
    Not set
    Midnight DriveNova Kane
    synthwave
    124F minor
    Not set
    Drums (stem)Nova Kane
    Not set
    124Not set
    Not set
    Voice memo 04
    Not set
    Not setNot set

Don't

  • Don't bury tempo and key in free-text tags; nothing there can be sorted, filtered or lined up.

    ArtworkTrackTagsPreviewBPMKey
    Not set
    Midnight DriveNova Kane
    synthwave124 BPMF minor
    Not setNot set
    Not set
    Paper LanternsIlya Sound
    ambient92bpmkey of C
    Not setNot set
  • Don't fill an absent field with a zero or an "n/a" — a stem is not 0 BPM, and the filler reads as a measurement.

    ArtworkTrackTagsPreviewBPMKey
    Not set
    Drums (stem)Nova Kane
    untagged
    124n/a
    Not set
    Voice memo 04
    untagged
    0n/a

Accessibility

Keyboard
  • A row's tab stops are decided entirely by which handlers you pass: none with neither, one with `onPlayToggle`, two with both. A hundred tracks with both wired is two hundred stops, in table order, with no way to skip a row.
  • Play/pause is a real `<button>`, so Enter and Space toggle it and it calls `onPlayToggle(id, !playing)`. Without `onSelect` the title is a plain `<span>`, so there is no keyboard route into a track at all.
  • The component installs no key handlers. There is no Space-plays-the-focused-row, no arrow-key movement between rows, no Escape to stop playback and no type-ahead.
  • The vendored table wraps itself in an `overflow-x-auto` container with no `tabIndex`, and every focusable control sits in the first four columns. At a narrow viewport BPM and Key scroll off to the right and no keyboard can bring them back — the two columns the component exists for become mouse-only.
  • Nothing is ever `disabled`. A track with no peaks and no audio still renders an enabled Play button whenever `onPlayToggle` is supplied.
Screen reader
  • It is a real `<table>` with `<th scope="col">` on every column, so each cell announces with its header. The artwork column's header is a visually hidden "Artwork" rather than an empty cell, and `label` lands as `aria-label` on the table itself.
  • The play control is named for its track — "Play Midnight Drive" / "Pause Midnight Drive" — so the announced name is the state, and the icon changes shape as well, which keeps it off colour.
  • An absent value renders through `NotSet`: the em-dash is `aria-hidden` and a visually hidden "Not set" is carried beside it, so an unknown BPM, key, tag set or cover announces the words rather than a dash, a blank or a zero.
  • The inline waveform is entirely `aria-hidden`, played portion included. `progress` moves the bars and nothing else, so how far through a track playback has reached is conveyed to nobody — as is the row's `data-state="playing"` tint.
  • There is no live region. Playback starting or stopping announces only through the Play button's own name changing, and only if that button still has focus; starting playback from anywhere else on the page is silent.
  • Tags render as `Badge` spans with no role, so they read as a run of plain text inside the Tags cell rather than as a list with a count.
  • The title control and the play control are siblings, never nested, so a row is two independent controls rather than one interactive inside another.
Focus
  • Nothing here mounts, unmounts or moves focus. A row does not change shape when it starts playing — the same controls stay in the same cells — so focus survives playback, and re-sorting `tracks` keeps focus with the same row's control as it moves, because rows are keyed by `track.id`.
  • Both controls take the vendored `Button`'s `focus-visible` ring. The artwork `PreviewTile` is rendered without `onSelect`, so it is an inert `<div>` and never a focus stop.
  • The only way to strand focus is your own data: dropping a track out of `tracks` while its Play button has focus leaves focus on `<body>`.

Watch out for

  • Auditioning must not navigate. If your onPlayToggle handler routes to a detail page, you have re-created the round trip this component exists to remove — start playback in place and leave the list mounted.
  • The play control renders only when onPlayToggle is supplied. A list that silently has no way to audition is usually a forgotten handler, not a decision.
  • progress applies to playingId alone. Setting it while nothing is playing does nothing, which normally means the playing id was not updated.
  • The waveform is decorative and hidden from assistive tech; it is not a scrubber and reports no position. If you need seeking, that belongs on a transport control, not on a 24px strip.
  • bpm is checked for being a number, not for truthiness — pass undefined for an unknown tempo, because 0 is a value and will render as one.
  • The title control and the play control are siblings, never nested. If you wrap the row in a link or a button to make it clickable, you have created a nested interactive and the accessibility gate will fail.
  • This is not H6 waveform-editor. If you want selection, zoom or regions, you want the editor; this strip is a preview you scan at row height.

Installation

npx shadcn@latest add https://super-ai-components.vercel.app/r/track-list.json

Requires a shadcn app on Base UI (style base-nova). Radix-based styles such as new-york fail to typecheck: these components use Base UI's render prop and callbacks like onOpenChangeComplete, which Radix primitives do not have.