Fader

A parameter slider built for live control panels: drag to tune a value while watching what it drives, with snap-to-point detents for discrete steps.

Interaction, crafted with intention.

Installation

pnpm dlx shadcn@latest add @aster/fader

Usage

Use cases

  • Live-adjustable parameters in a control panel, like opacity, radius, or scale, where feeling the change as you drag matters more than typing an exact number.
  • Snap-to-detent choices (points={[0, 4, 8, 16]}) when only a few values actually make sense, without giving up the scrub feel.
  • Anywhere a bare numeric input would hide the value's relationship to the range it lives in.

Examples

What it is

A parameter fader: the control doubles as the value display. It takes its name and behavior from a mixing console's fader, where the label and live value sit inside the fill itself, so the fill's edge is always the reading. Built for tuning a value while watching it change, not for picking one value in a form.

Built on

Fader is built on Base UI's Slider (@base-ui/react), which supplies the semantics, keyboard handling, touch support, and ARIA plumbing. It may move onto Aster's own slider foundation in the future, but its public API won't change.

Interaction spec

Two value grammars, explicit in the API and never merged implicitly:

  • Continuous (default): "about this much." Drag is 1:1 with the pointer; a click teleports, so the travel animates on a spring to read as continuous motion.
  • Snap points (points={[0, 4, 8, 16]}): "one of these." Pointer input only ever lands on a point. Dragging hops between detents on a spring, clicks go to the nearest point, and a mark shows each interior point. Coarse-step sliders (≤ 12 stops) behave as detents too; snappy is one behavior with two ways to declare it.

Pointer and drag:

  • Hover → fill brightens one step (fine pointers only)
  • Pointer down on track → grab; drag → value follows 1:1 (or hops detents)
  • Click → value teleports; the fill springs to it
  • Drag past an end → elastic stretch, spring home on release or re-entry

Keyboard:

  • → step (or previous/next point)
  • Home / End → min / max
  • PageUp / PageDown → large step

Caveats

  • Fader is controlled-only: there's no internal fallback state, value must always be owned by the parent.
  • points needs at least 2 entries to form a grammar; fewer than that logs a dev-time warning, not an error.
  • After a drag ends, Fader gives a deferred parent about 300ms to catch up before snapping back to the accepted value. Rejecting a change slower than that will look like the drag briefly overshot.

Accessibility

  • Base UI's slider supplies a real input type="range": native slider semantics, no ARIA re-implementation.
  • One tab stop (the Apple HIG model): Tab focuses the slider, arrows adjust. Keyboard changes are never animated.
  • The visual label/value overlay is aria-hidden to avoid double announcements; the input already owns the accessible name and value via aria-labelledby.
  • aria-valuetext includes the unit, so a screen reader hears "65%", never a bare "65".
  • Focus ring on the outer control, keyboard-only (:focus-visible via has-focus-visible), plus a full-presence bar: one ring language everywhere.
  • touch-pan-y: vertical swipes scroll the page; horizontal gestures reach the slider. Track heights (32/40/48px) clear the WCAG 2.5.8 target minimum.
  • disabled uses Base UI's native model (data-disabled, unfocusable).

Motion

Travel (click-jump, detent hops) runs on springs.settle with carried velocity: a second input mid-flight retargets the same spring instead of cutting. Drag and keyboard input are never animated. Hover and drag tint the fill at --motion-dur-fast. The elastic overdrag is transform-only (scaleX, ≤ 8px, exponential damping) and springs home.

Under prefers-reduced-motion, springs become jumps, the bar keeps its opacity feedback but drops scale, and the overdrag stretch is disabled entirely.

API

Fader

PropTypeDefaultDescription
label
string
Accessible name, rendered inside the track.
value
number
Controlled value, the single source of truth.
onValueChange
(value: number) => void
Change handler for drag, click, and keyboard input.
min / max / step
number
0 / 100 / 1Range and step. ≤ 12 reachable stops behave as detents.
points
number[]
Snap-points grammar: input only ever lands on these values.
unit
string
Rendered after the value and spoken via aria-valuetext.
size
sm|md|lg
"md"Track height scale; bar and marks scale along.
tone
accent|neutral
"accent"Ink family for fill and bar, from the color roles.
bordered
boolean
falseHairline input outline for busy surfaces.
disabled
boolean
falseDisables interaction; dims to the disabled opacity.

useFader

The behavior layer: value grammars, the settle spring and its controlled contract, text-zone measurement, bar dodge, and elastic overdrag. Owns every motion value and renders nothing; the visual component is variants and markup over it.

Did you like the content?