# 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:

```css
.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`:

```css
.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: none` on a surface that is dragged, so a touch drags the control and does not scroll the page.
- `position: relative` on 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: none` on 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:

```css
/* 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.

{/* reference:start */}

### 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` |

{/* reference:end */}
