Skip to main content

Styling

tremolo-ui ships no CSS. A component renders its markup, keeps track of the value, and handles the pointer, keyboard and wheel; what it looks like is yours. Style it with vanilla CSS, CSS Modules, Tailwind, CSS-in-JS — whatever the rest of your app already uses.

🎨Class names

Every element carries a class beginning with tremolo-, so plain CSS reaches it without any wrapper of your own.

.tremolo-knob-active-line {
color: rebeccapurple;
}

The names follow the component and the part: tremolo-slider, tremolo-slider-track, tremolo-slider-thumb, tremolo-slider-marks. The theme below lists all of them.

🚦State

State is on the element as an attribute, not as an extra class, so it is a selector away.

AttributeWhereMeaning
[aria-disabled="true"]every componentdisabled is set
[aria-readonly="true"]every componentreadonly is set
[data-dragging="true"]Knob, PointsEditor.Pointa drag is in progress
[data-vertical="true"]Slider, Slider.Track, Slider.Marks, Slider.MarksOptionthe slider runs vertically
[data-flipped="true"]Slider.Trackthe value grows from the far end (vertical or reverse, not both)
[data-active="true"]Piano keysthe note is sounding
[data-out-of-range="true"]NumberInput.InputFieldthe value is outside min/max
[data-fill="true"]Pianofill is set
[data-selected="true"]PointsEditor.Pointthe point is in the selection
.tremolo-slider[aria-disabled='true'] {
opacity: 0.5;
}

:focus, :hover and :focus-within work as they would anywhere, since the focusable element is a real one.

📐Custom properties

Anything about a component that is a matter of appearance is a custom property, and the prop that goes with it does nothing but write that property.

<Slider.Track thickness={16} />
// is exactly
<Slider.Track style={{ '--thickness': '16px' }} />

So the prop, a stylesheet and an inline style are three ways to say one thing, and the usual cascade decides between them. The default is in the CSS, not in the component, so a rule of yours is enough to change it everywhere — you do not have to pass a prop to every instance.

.tremolo-slider-track {
--thickness: 16px;
}

A number passed as a prop is taken as pixels; anything else is written through as-is, so '3rem', '100%' and 'auto' all work.

ComponentProperties
Knob--knob-size
Slider.Track--length, --thickness, --active, --inactive
Slider.Thumb--thumb-size (on Slider), --color
Slider.Marks--gap
Slider.MarksOption--thickness, --length, --gap, --label-width
XYPad.Area--width, --height, --min-width, --min-height, --color
XYPad.Thumb--thumb-size (on XYPad), --color
PointsEditor--width, --height
PointsEditor.Point--width, --height, --color
PointsEditor marquee--color on .tremolo-points-editor-marquee
Piano--height, and the per-key colours in the theme below

--percent, and drawing the fill yourself

Slider.Track also publishes --percent, where the value currently sits. It is the one number CSS cannot work out on its own, and it is why the component draws nothing: the fill is a rule in the theme rather than something the package paints for you.

.tremolo-slider-track {
background: linear-gradient(
var(--axis),
var(--active) var(--percent),
var(--inactive) var(--percent)
);
}

Replace that rule and the track can be anything — a second colour stop, an image, a dotted scale that fills in.

📋The theme used on this site

Every example in these docs is styled by the files below. They are the starting point rather than a dependency: copy the ones you need into your own project and edit them.

Each is plain CSS with no build step, and each one leans on custom properties (--knob-size, --thumb-size, --active-color …) so the common changes are a one-line override.

Knob.css
.tremolo-knob {
/* The knob has no intrinsic size: the SVG inside scales to its box.
Override with the `size` prop, or by setting this variable. */
--knob-size: 50px;

display: inline-block;
width: var(--knob-size);
height: var(--knob-size);
/* A knob is a fixed-size control, like a button. Without this a flex row
that runs short shrinks the width and leaves the height alone, so the box
stops being square and the knob sits in it with dead space below.
`aspect-ratio` is not the answer: it is ignored while both width and
height are definite, and dropping the height to free it up lets
`align-items: stretch` pull the box the other way. */
flex-shrink: 0;
cursor: grab;
outline: 0;

&[aria-readonly='true'] {
cursor: not-allowed;
}

&[data-dragging='true'] {
cursor: inherit;
}
}

.tremolo-knob-active-line {
color: oklch(69.2% 0.17548 266.373);

:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
color: oklch(59.5% 0.17548 266.373);
}

:where(.tremolo-knob[aria-disabled='true']) & {
color: oklch(50.5% 0.03307 270.021);
}
}

.tremolo-knob-inactive-line {
color: oklch(95% 0 0);

:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
color: oklch(90% 0 0);
}
}

.tremolo-knob-thumb {
color: oklch(85.7% 0 0);

:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
color: oklch(84.5% 0 0);
}
}

.tremolo-knob-thumb-line {
color: oklch(94.2% 0 0);
}

:where(.dark, [data-theme='dark']) {
.tremolo-knob-active-line {
color: oklch(59.5% 0.17548 266.373);

:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
color: oklch(65.2% 0.17548 266.373);
}
}

.tremolo-knob-inactive-line {
color: oklch(41.7% 0 0);

:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
color: oklch(44.59% 0 0);
}
}

.tremolo-knob-thumb {
color: oklch(48.6% 0 0);

:where(.tremolo-knob:focus, .tremolo-knob:hover) & {
color: oklch(51.7% 0 0);
}
}

.tremolo-knob-thumb-line {
color: oklch(84.2% 0 0);
}
}

🧩Component-based styling

Nothing says the styles have to be global. Here is the same knob with CSS Modules, passing a class to the part it belongs to.

Loading...
import { useState } from 'react'

import { Knob } from '@tremolo-ui/react'

import myKnob from './my-knob.module.css'

function App() {
  const [value, setValue] = useState(64)

  return (
    <div className={myKnob.container}>
      <Knob.Root
        className={myKnob.knob}
        value={value}
        min={0}
        max={100}
        size={50}
        onChange={(v) => setValue(v)}
      >
        <Knob.SVGRoot>
          <Knob.ActiveLine className={myKnob.activeLine} />
          <Knob.InactiveLine />
          <Knob.Thumb />
        </Knob.SVGRoot>
      </Knob.Root>
      {value}
    </div>
  )
}

export default App

🧭Next Step

Let's explore the components offered by tremolo-ui.
Components - Knob