Skip to content

Slider ​

Drag a handle along a rail to pick a number, or two handles to pick a range. Aurora re-exports reka-ui's Slider primitives unchanged so you import them from one place — but there is no Aurora slider component: no variants, no default chrome, no tokens applied for you. Every visible pixel is a class you write. Five call sites across web-app already write the same classes, so copy those rather than inventing a new look.

ts
import { SliderRange, SliderRoot, SliderThumb, SliderTrack } from '@scaler-tech/aurora/slider'
import type { SliderRootEmits, SliderRootProps } from '@scaler-tech/aurora/slider'

Single thumb

Target year2035
20252050

Two thumbs

Floor area5,000 – 32,000 m²
0 m²50,000 m²

Composition ​

SliderRoot owns the value and the geometry; the three children are pure presentation.

  • SliderRoot — takes v-model (an array), min, max, step. Positions the thumbs.
  • SliderTrack — the full-width rail. Needs relative so the range can absolutely position inside it.
  • SliderRange — the filled segment from the minimum (or from the lower thumb) up to the value. Must be inside SliderTrack.
  • SliderThumb — one per value in the model array; use v-for once you have more than one.

The primitives add no props of their own, so reka-ui's Slider docs are the prop reference (orientation, inverted, minStepsBetweenThumbs, disabled, …).

The house styling ​

This is the markup behind the range in the demo above, and it matches FilterSlider.vue, FilterDataCoverage.vue, CapexScenario.vue, ExitValueSlider.vue, and ForecastDriversCardV3.vue byte for byte. For a single thumb, drop the v-for and pass a one-element array.

vue
<SliderRoot
    :model-value="[localMin, localMax]"
    :min="bounds.min"
    :max="bounds.max"
    :step="1"
    class="relative flex items-center select-none touch-none w-full h-5"
    @update:model-value="handleSliderUpdate"
>
    <SliderTrack class="relative bg-disabled grow rounded-full h-1">
        <SliderRange class="absolute bg-cta rounded-full h-full" />
    </SliderTrack>
    <SliderThumb
        v-for="i in 2"
        :key="i"
        class="block w-4 h-4 bg-white border-2 border-cta rounded-full focus:outline-hidden focus:ring-2 focus:ring-focus shadow-100 hover:bg-secondary-hover"
    />
</SliderRoot>

Two classes in there are structural rather than cosmetic: relative on SliderTrack is what keeps the absolutely-positioned SliderRange inside the rail instead of escaping to the nearest positioned ancestor, and touch-none on the root stops a drag from scrolling the page on touch devices (select-none stops text selection during a fast drag).

A slider alone tells the user nothing about where the handle landed, so every call site pairs it with a readout — the current value above, and the min/max labels below in paragraph-xs text-secondary tabular-nums.

Gotchas ​

  • v-model is an array even for a single thumb ([50], not 50), and @update:model-value hands back number[] | undefined — bind a bare number and the slider silently ignores it. Guard the undefined and the length before indexing. [external]
  • SliderThumb renders a bare unstyled element, invisible until you give it size and background — block w-4 h-4 bg-white … is load-bearing, not decoration. [external]
  • One thumb renders per value. Fewer thumbs than values means a value nobody can drag; more means a dead handle. [external]
  • Binding :model-value to debounced parent state makes the thumb lag the cursor, since drag fires per pixel. Keep a local ref for the thumb, debounce only the emit outward, and watch the prop to sync back.