Skip to contentVibraUI
Inputs & filters

Slider

A track with one thumb for a value, or two for a range.

Vibra draws the thumb as a card-plane disc with the --input boundary that turns the accent on hover, where shadcn's is a white disc ringed in --ring with a halo; it takes the kit's solid focus outline, drawn on the thumb while the hidden range input inside it has the focus — the thumb itself never does — and the filled part of the track is the accent through --primary. It draws a thumb per value, from a number or an array, each told its place so a range renders whole on the server. Name it with aria-labelledby or aria-label and describe it with aria-describedby: all three reach the thumbs, where the focus is, whereas Base UI hands a thumb the labelledby alone. getAriaLabel names a range's thumbs apart, and getAriaValueText or format reads the value in its unit — "$60", "90 days" — rather than as a bare number.

Install

npx shadcn@latest add @vibra/slider

Needs the @vibra registry in your components.json — set it up once.

Examples

Stepped, with its marks

Retention periods that aren't evenly spaced: the thumb moves through their places and reads "90 days", never "2".

Words at the ends

A setting without a unit: the ends are named, and the thumb reads the number with what it leans towards.

Icons at the ends

A slow and a fast picture either side, hidden; the thumb reads "1.2 times normal speed".

A range of times

Two thumbs named for their ends and read as times of day, kept at least two hours apart.

A price range over its orders

The range filter over the distribution it cuts: the bars it keeps are drawn in the ink, and the count under it follows.

With − and + buttons

A step at a time either side; at the ends the buttons stay focusable and point at the line that says why they stop.

Vertical

An equaliser of three upright sliders, each named by the label under it and read in decibels with its sign.

A threshold marked

A mark on the track where the rule changes, and past it a line that says who signs off, as the thumb's description.

Applies on release

The readout follows the drag while the query waits for onValueCommitted, and a status line says when it has run.

Disabled, with the reason

A limit set by someone else still shows where it stands, and the line under it says who set it and where it changes.

Right to left

The demo's alert in Arabic, in a DirectionProvider: the fill grows from the right and the left arrow raises the value.

Props

PropTypeDefaultDescription
value / defaultValuenumber[]—One entry per thumb: [80] draws one, [60, 300] a range.
min / max / stepnumber0 / 100 / 1The scale and its steps; a scale that isn't evenly spaced moves through indexes and names each in getAriaValueText.
largeStepnumber10How far Page Up, Page Down and Shift with an arrow move, in the value's own units.
minStepsBetweenValuesnumber0The least gap between a range's thumbs, counted in steps.
orientation"horizontal" | "vertical""horizontal"Upright for a mixer or a level; give the slider's box a height.
getAriaLabel(index: number) => string—A name per thumb — "Lowest order value", "Highest order value" — so a range's two ends are told apart.
getAriaValueText(formatted: string, value: number, index: number) => string—What a screen reader hears for a value, in its unit: "$60", "90 days", "6:30 AM".
formatIntl.NumberFormatOptions—Formats the value for the thumb when getAriaValueText is not given, e.g. a currency or a percent unit.
onValueCommitted(value) => void—Fires once, on release or on a key, where onValueChange fires on every step of a drag.
disabledbooleanfalseDims the slider and takes its thumbs out of the tab order, so write why beside it.

Dependencies

Source

components/ui/slider.tsx
import { Slider as SliderPrimitive } from "@base-ui/react/slider"
import { cn } from "@/lib/utils"

type SliderProps = SliderPrimitive.Root.Props &
  Pick<SliderPrimitive.Thumb.Props, "getAriaLabel" | "getAriaValueText">

function Slider({
  className,
  defaultValue,
  value,
  min = 0,
  max = 100,
  getAriaLabel,
  getAriaValueText,
  "aria-label": ariaLabel,
  "aria-describedby": ariaDescribedBy,
  ...props
}: SliderProps) {
  // One thumb per value. A plain number is one value too: read as neither
  // array, it fell through to [min, max] and drew two thumbs on the same spot.
  const _values = Array.isArray(value)
    ? value
    : typeof value === "number"
      ? [value]
      : Array.isArray(defaultValue)
        ? defaultValue
        : typeof defaultValue === "number"
          ? [defaultValue]
          : [min, max]

  return (
    <SliderPrimitive.Root
      className={cn("data-horizontal:w-full data-vertical:h-full", className)}
      data-slot="slider"
      defaultValue={defaultValue}
      value={value}
      min={min}
      max={max}
      thumbAlignment="edge"
      {...props}
    >
      <SliderPrimitive.Control className="relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:min-h-40 data-vertical:w-auto data-vertical:flex-col">
        <SliderPrimitive.Track
          data-slot="slider-track"
          className="relative grow overflow-hidden rounded-full bg-muted select-none data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1"
        >
          <SliderPrimitive.Indicator
            data-slot="slider-range"
            className="bg-primary select-none data-horizontal:h-full data-vertical:w-full"
          />
        </SliderPrimitive.Track>
        {Array.from({ length: _values.length }, (_, index) => (
          <SliderPrimitive.Thumb
            data-slot="slider-thumb"
            key={index}
            // Its place among the thumbs, known before mount, so a range
            // renders each thumb with its value on the server.
            index={index}
            // Base UI hands a thumb the root's aria-labelledby and nothing
            // else. The name, the description and the words for the value
            // belong on the thumb, where the focus is.
            aria-label={ariaLabel}
            aria-describedby={ariaDescribedBy}
            getAriaLabel={getAriaLabel}
            getAriaValueText={getAriaValueText}
            // The focus lands on the visually hidden range input inside the
            // thumb, never on the thumb itself, so the thumb draws the kit's
            // outline for it: 2px in the accent, 2px off the disc.
            className="relative block size-3 shrink-0 rounded-full border border-input bg-card shadow-(--shadow-sheet) transition-[border-color,box-shadow] duration-(--duration-fast) ease-(--ease-standard) outline-none select-none after:absolute after:-inset-2 hover:border-brand has-[:focus-visible]:focus-outline disabled:pointer-events-none disabled:opacity-50"
          />
        ))}
      </SliderPrimitive.Control>
    </SliderPrimitive.Root>
  )
}

export { Slider }