Slider

A slider picks a number, or a range of two, by dragging or with the keyboard.

import { Slider } from "@/components/ui/slider";

export default function SliderDemo() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-8">
      <Slider defaultValue={40} label="Volume" />
      <Slider defaultValue={[20, 70]} label="Price" />
      <Slider defaultValue={50} label="Brightness" disabled />
    </div>
  );
}

The code

The component is a single file, read from the repository when the site is built. Press Show code to see all of it.

import type { ComponentProps } from "react";
import { Slider as BaseSlider } from "@base-ui/react/slider";
import { cx } from "@/lib/cx";

// The focused element is the hidden range input inside the thumb, not the thumb itself, so the ring
// is drawn when the thumb *contains* keyboard focus rather than with focus-visible on the thumb.
const thumbRing =
  "outline-hidden has-[:focus-visible]:outline-2 has-[:focus-visible]:outline-solid has-[:focus-visible]:outline-offset-2 has-[:focus-visible]:outline-text";

// Pass a number for one thumb or an array of two for a range. Every thumb needs a name for
// assistive tech, so `label` is required; a range gets "minimum" and "maximum" added to it.
export function Slider({
  label,
  className,
  ...props
}: Omit<ComponentProps<typeof BaseSlider.Root>, "className" | "children"> & {
  label: string;
  className?: string;
}) {
  const current = props.value ?? props.defaultValue ?? 0;
  const thumbs = Array.isArray(current) ? current.length : 1;

  return (
    <BaseSlider.Root className={cx("w-full", className)} {...props}>
      <BaseSlider.Control className="flex h-5 w-full touch-none items-center select-none">
        <BaseSlider.Track className="relative h-1.5 w-full rounded-full bg-border-strong select-none">
          <BaseSlider.Indicator className="rounded-full bg-accent data-[disabled]:bg-text-secondary" />
          {Array.from({ length: thumbs }, (_, index) => (
            <BaseSlider.Thumb
              key={index}
              index={thumbs > 1 ? index : undefined}
              aria-label={thumbs > 1 ? `${label} ${index === 0 ? "minimum" : "maximum"}` : label}
              className={cx(
                "block size-5 cursor-grab rounded-full bg-text transition-transform duration-150",
                thumbRing,
                "data-[dragging]:scale-110 data-[dragging]:cursor-grabbing",
                "data-[disabled]:cursor-not-allowed data-[disabled]:bg-text-secondary",
              )}
            />
          ))}
        </BaseSlider.Track>
      </BaseSlider.Control>
    </BaseSlider.Root>
  );
}

Props

PropTypeDefaultWhat it does
labelstring—Required. The name of what the slider controls. A range adds "minimum" and "maximum" to it.
defaultValuenumber | number[]—The starting value. An array of two makes a range with two thumbs.
valuenumber | number[]—The value, when you control it yourself.
min, max, stepnumber0, 100, 1The smallest and largest value, and the size of one step.
disabledbooleanfalseTurns the slider off.
classNamestring—Extra classes, added after the slider's own.

Every other prop, such as onValueChange, goes to the Base UI Slider underneath.

The thinking

A slider is for a value where roughly right is good enough, like volume. For an exact number, an input is better.

  • Built on Base UI: It handles dragging, clicking the track, keyboard steps and the value itself. This file draws the track and thumb.
  • A range is an array: Pass [20, 70] and you get two thumbs and a filled stretch between them. A single number gives one thumb.
  • Every thumb has a name: The label is required, so a slider can't be shipped without one.

Interface design

  • Track: 6px tall and fully rounded, in the strong border color. The filled part is the accent, 7.7:1 against the track.
  • Thumb: A 20px circle in the main text color.
  • Disabled: The filled part and the thumb both turn grey.

Interaction

  • Pointer: Drag the thumb, or click the track to jump there. The thumb grows slightly while it is being dragged, and the cursor becomes a closed hand.
  • Keyboard: The arrow keys move one step, Home and End jump to the ends.
  • Focus: The focused thumb draws a 2px outline, 2px away.
  • Disabled: It can't be dragged or focused, and shows a not-allowed cursor.

Empathy

  • Announced: Each thumb is read as a slider with its name, its value and its limits.
  • No printed value: This slider doesn't show a number. If the exact value matters, show it in text next to the slider.
  • Target size: The thumb is 20px, under the 24px minimum of WCAG 2.2 AA. The whole track also responds to a click, and the arrow keys give a precise alternative to dragging.