VI Controls

Star on GitHubFeedback
VI ControlsNative HTML and a hookot-ruler

Slider

A ruler with a hand. The value is drawn as a dimension from zero with your number in italic in the gap, or runs through the label's spread letters, or makes the label as loud as the value.

Report an issue with Slider

Example

64
58%
40
Rest
40
Too short a gap
6
Focus
40
Disabled
40
spread, rest
35
spread, focus
35
dynamics, quiet
8
dynamics, loud
92

Install

Run it in a project that has the shadcn CLI set up. It brings the 0nlyType base along.

npx shadcn@latest add https://thedirectors.agency/ui/r/slider.json

Usage

slider-example.tsx
import { Slider } from "@/components/ui/slider"export default function Example() {return (<div className="grid max-w-[36rem] gap-y-9"><Slider label="Volume" defaultValue={64} /><Slider variant="spread" label="Brightness" defaultValue={58} unit="%" /><Slider variant="dynamics" label="Volume" defaultValue={40} /></div>)}

Contract

  • Underneath: native range, plus a hook for the readout.
  • Anatomy: .ot-ruler[data-variant="dimension | spread | dynamics"] holding a label, an <output>, .ot-ruler-hand (the <input type="range">, and for spread .ot-ruler-letters) and .ot-ruler-scale of <span style="--n: 0…1">. Script sets --p (0–1). Spread and dynamics need a string label; they keep it in the DOM as .ot-sr and draw it aria-hidden.
  • The track is a ruler: minor ticks every 5%, major every 25%, and the filled part in ink. The thumb is an ink hairline topped by the accent dot (--h tall).
  • Dimension (the default). Yours: the value, a large italic numeral set in the gap of a dimension line (after Paul Rand), .ot-ruler-value. The line runs from zero to the thumb, with a tick at each end, and always stays true to the hand: when the gap is too short for the figure, script sets data-outside and the figure stands outside, past the far tick, as a draughtsman sets it.
  • Spread. After "Renaissance.", whose words are spread to the edges: the label's letters, in capitals, are the ruler's marks, set out across its whole length over a single baseline (--h: 64px). The hand stands through them and inks every letter it has passed, splitting the one it stands in, as "the uncreative" splits its word (background-clip: text). Your number, .ot-ruler-corner, waits in the far corner; the scale keeps only its two ends.
  • Dynamics. The score marks loudness pp to ff in italic under the notes. Here the label, .ot-ruler-loud, is as loud as the value: its weight runs 100 to 900 and its width 62% to 125% with the hand (as far as the pair's voice allows), with your number after it; its size is set from its letter count and the ruler's width (a size container), so at its heaviest it still fits one line and nothing reflows while you drag. The scale reads pp p mp mf f ff as .ot-term, and the nearest marking inks (data-on).
  • The value and unit are one text node, isolated left to right; a shorter number never shifts a separate unit node. The ruler and the figure's placement still follow the page direction.
  • While the thumb is held (:active), the value lifts 5px, and it drops back with spiccato when you let go.
  • States: rest, focus (outline 8px out), disabled (45% opacity). Right to left, zero is on the right, the fill, the ink through the letters and the outside figure follow. Forced colours: the ruler restates its tokens in system colours (CanvasText, GrayText, Highlight for the dot).
  • Keyboard: arrows step, Page Up / Page Down jump, Home / End go to the ends.

Props

Props of Slider
PropTypeDefaultWhat it does
labelReactNodeWhat is being measured. It labels the range. spread and dynamics draw it, so pass a string there.
variant"dimension" | "spread" | "dynamics""dimension"How the value is drawn. dimension: a dimension line from zero to the hand with your number in its gap, and past the far tick when the gap is too short for it. spread: the label's letters, in capitals, are the ruler's marks, spread across its length; the hand inks every letter it passes, splitting the one it stands in, and your number waits in the far corner. dynamics: the label is as loud as the value, its weight and width following the hand, over a scale from pp to ff whose nearest marking inks.
min / max / stepnumber0 / 100 / 1The range. Ticks fall every 5%, labelled every 25%.
value / defaultValuenumberThe current value.
onValueChange(value: number) => voidCalled with the new value as the hand moves.
unitstringSet after the number as written: " kg" with its space, "%" without.
disabledbooleanfalseThe ruler fades and the hand stays put.
classNamestringClasses for the ruler's outer div; the input props apply to the native range inside it.

Craft

Where its move comes from

Where its move comes from
ComponentReferenceMove
SliderPaul Rand dimension linesValue set in a dimension from zero
Spread slider"Renaissance."; "the uncreative"The label's letters are the ruler's marks; the hand inks them
Dynamics sliderThe score's dynamicsThe label is as loud as the value

How it moves

How it moves
ComponentArticulationWhat moves
SliderLiftThe value rises while the hand is on it
Spread sliderInk followsThe ink runs through the letters with the hand
Dynamics sliderSwellThe word grows heavier and wider with the hand