Arrangement
Transport and mixer
EQ and filters
Modulation
Utility
Instruments
Headless styling
How to style the primitives of @audio-ui/react with nothing but CSS: each part exposes its state as data attributes and its live values as CSS variables, and the reference below lists them for every primitive.
The primitives paint nothing: no colour, no size, no theme. What a part looks like comes from your CSS, and the part gives that CSS everything it needs: its state as attributes, and what changes too often to be an attribute as a variable. A skin needs no React state of its own.
Style from the state
A part carries its state on the element. A knob that is being dragged has data-dragging, a disabled fader data-disabled, a fader a data-orientation. Select on them:
.my-knob [data-dragging] {
box-shadow: 0 0 0 4px color-mix(in oklab, currentColor 20%, transparent);
}
.my-knob [data-disabled] {
opacity: 0.4;
}A boolean that is off has no attribute, so the absence is a state too: :not([data-active]) styles the rest.
Style from the variables
What changes on every frame is a CSS variable, set on the element, not an attribute, so the browser does not re-match selectors on each change. A knob's mark turns with --knob-mark-angle, a pad's crosshair moves with --xy-pad-x and --xy-pad-y:
.my-knob-mark {
transform: rotate(var(--knob-mark-angle));
}What stays inline
Three styles are behaviour, not look, and each part sets them itself:
touch-action: noneon a surface that is dragged, so a touch drags the control and does not scroll the page.position: relativeon a container that holds parts placed absolutely, such as a fader's mark or a knob's arc, so those parts sit in the container and not on the page.pointer-events: noneon decoration that must not take the pointer.
A stylesheet can still change anything else about them. The behaviour of these three is the primitive's contract.
An inline style beats any class, so a class cannot change them. To place a part itself, say an envelope at the corner of a panel, pass the style prop: it is applied after the part's own, so style={{ position: "absolute", top: 8 }} wins. In a skin of CSS alone, put the part in an element you position.
What a part sets inline for its value, such as a thumb's left and translate or an indicator's height, is the value itself: a class does not replace it, it adds to it.
Give each part a size
A part has no size of its own, and one that sits at a position along a line or a plane does not lay itself out: it needs a box, and for a part placed with bottom, left or inset, position: absolute. These are the least that draws each of the five controls below, from the part names of their anatomy:
/* Fader: the control is the box, the thumb sits at the value */
.fader-control { width: 2rem; height: 8rem; }
.fader-track { width: 0.5rem; height: 100%; margin-inline: auto; background: #333; }
.fader-indicator { position: absolute; background: #6cf; }
.fader-thumb { width: 1.5rem; height: 0.75rem; background: #eee; }
/* XYPad: the control is the surface */
.xypad-control { width: 8rem; height: 8rem; background: #222; }
.xypad-thumb { position: absolute; width: 1rem; height: 1rem; border-radius: 50%; background: #6cf; }
/* Knob: the arcs are SVGs that fill the control, one over the other */
.knob-control { width: 4rem; height: 4rem; }
.knob-track,
.knob-indicator { position: absolute; inset: 0; width: 100%; height: 100%; }
.knob-track { color: #333; }
.knob-indicator { color: #6cf; }
.knob-thumb { position: absolute; inset: 0; } /* turns with the value */
.knob-thumb::after { content: ""; position: absolute; top: 12%; left: 50%; width: 2px; height: 28%; background: #eee; transform: translateX(-50%); }
/* Meter: the channel is the bar, the indicator is scaled to the level */
.meter { height: 8rem; display: flex; gap: 2px; }
.meter-channel { width: 0.75rem; height: 100%; background: #222; }
.meter-indicator { position: absolute; bottom: 0; background: #6c6; }
/* Plot: the root is the box, the graph fills it */
.plot { width: 12rem; height: 6rem; background: #222; }
.plot-line { stroke: #6cf; }Each class goes on the part it is named after, with className. Sizes are in your units and colours are yours; what matters is which part takes which rule.
Reference
Generated from the source of each primitive: the data attributes of the state each part exposes, and the CSS variables it sets. A value of the state of a plain type (a boolean, a number, a string) is an attribute, unless the primitive keeps it off the element. A callback or an object is not.
Envelope
State on every part that takes it: data-disabled, data-dragging.
| Part | Data attributes | CSS variables |
|---|
Fader
State on every part that takes it: data-disabled, data-dragging, data-orientation.
| Part | Data attributes | CSS variables |
|---|
Knob
State on every part that takes it: data-disabled, data-dragging.
| Part | Data attributes | CSS variables |
|---|---|---|
Mark | --knob-mark-angle |
Meter
| Part | Data attributes | CSS variables |
|---|---|---|
Root | data-clipped |
Pad
State on every part that takes it: data-columns, data-disabled.
| Part | Data attributes | CSS variables |
|---|---|---|
Item | --pad-velocity |
Piano
State on every part that takes it: data-disabled, data-max-pitch, data-min-pitch, data-orientation.
| Part | Data attributes | CSS variables |
|---|
Piano-roll
State on every part that takes it: data-disabled, data-draw-style, data-folded, data-max-pitch, data-min-length, data-min-pitch, data-mode.
| Part | Data attributes | CSS variables |
|---|---|---|
Root | data-has-selection | |
Rows | data-in-scale |
Plot
State on every part that takes it: data-disabled, data-max-x, data-max-y, data-min-x, data-min-y.
| Part | Data attributes | CSS variables |
|---|---|---|
Cursor | data-active | --plot-cursor-x, --plot-cursor-y |
Sequencer
State on every part that takes it: data-disabled.
| Part | Data attributes | CSS variables |
|---|---|---|
Root | data-current, data-painting | |
Velocity | --sequencer-velocity |
Timeline
State on every part that takes it: data-default-position, data-default-zoom, data-disabled, data-duration, data-large-step, data-max-zoom, data-min-zoom, data-position, data-snap-offset, data-snap-step, data-step, data-value, data-zoom.
| Part | Data attributes | CSS variables |
|---|---|---|
ClipFade | --clip-fade-in, --clip-fade-out | |
Clip | --clip-drag-y, --clip-fade-in, --clip-fade-in-max, --clip-fade-out, --clip-fade-out-max, --clip-offset, --clip-source-length | |
Lane | data-timeline-lane |
Waveform
| Part | Data attributes | CSS variables |
|---|---|---|
Bars | data-index, data-played | --waveform-level |
Canvas | data-theme | --waveform-bar-radius, --waveform-color, --waveform-progress-color |
Root | data-active, data-outside |
Xypad
State on every part that takes it: data-disabled, data-dragging.
| Part | Data attributes | CSS variables |
|---|---|---|
Root | --xy-pad-x, --xy-pad-y |