Slider

An easily stylable range input.

"use client"

import { Slider as SliderPrimitive } from "@animate-ui/react/primitives/base-ui/slider"
import { cn } from "cn"

function Slider({
  className,
  defaultValue,
  value,
  min = 0,
  max = 100,
  ...props
}: SliderPrimitive.Root.Props) {
  const _values = Array.isArray(value)
    ? value
    : Array.isArray(defaultValue)
      ? 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="data-vertical:min-h-40 relative flex w-full touch-none items-center select-none data-disabled:opacity-50 data-vertical:h-full data-vertical:w-auto data-vertical:flex-col">
        <SliderPrimitive.Track
          data-slot="slider-track"
          className="bg-muted rounded-full data-horizontal:h-1 data-horizontal:w-full data-vertical:h-full data-vertical:w-1 relative grow overflow-hidden select-none"
        >
          <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}
            className="border-ring ring-ring/50 relative size-3 rounded-full border bg-white transition-[color,box-shadow] after:absolute after:-inset-2 hover:ring-3 focus-visible:ring-3 focus-visible:outline-hidden active:ring-3 block shrink-0 select-none disabled:pointer-events-none disabled:opacity-50"
          />
        ))}
      </SliderPrimitive.Control>
    </SliderPrimitive.Root>
  )
}

export { Slider }

export default function SliderDemo() {
  return (
    <Slider
      defaultValue={[75]}
      max={100}
      step={1}
      className="mx-auto w-full max-w-xs"
    />
  )
}

Installation

Install the packages

npm install @animate-ui/react motion
pnpm add @animate-ui/react motion
yarn add @animate-ui/react motion
bun add @animate-ui/react motion

Add the shadcn/ui component

npx shadcn@latest add slider
pnpm dlx shadcn@latest add slider
yarn dlx shadcn@latest add slider
bun x shadcn@latest add slider

Animate it

The CLI finds your shadcn/ui components through components.json, swaps their primitive imports for Animate UI's and removes the CSS animation classes Motion replaces.

npx @animate-ui/cli@latest animate slider
pnpm dlx @animate-ui/cli@latest animate slider
yarn dlx @animate-ui/cli@latest animate slider
bun x @animate-ui/cli@latest animate slider

Usage

Import the primitive from Animate UI instead of Base UI. Everything else stays the same.

import { Slider } from '@animate-ui/react/primitives/base-ui/slider';

export function Volume() {
  return (
    <Slider.Root defaultValue={50}>
      <Slider.Control>
        <Slider.Track>
          <Slider.Indicator />
          <Slider.Thumb aria-label="Volume" />
        </Slider.Track>
      </Slider.Control>
    </Slider.Root>
  );
}

Using the @base-ui/react umbrella import instead? The barrel mirrors it: import { Slider } from '@animate-ui/react/primitives/base-ui'.

Animation

Animate UI v1 had no slider: it moves like v1's switch thumb.

  • The pressed or dragged thumb stretches along the track: its width grows by 3/16 (a 16px thumb becomes 19px, v1's switch pressedWidth), on a spring (stiffness 400, damping 32), and springs back on release. It's the thumb's real width, so its round ends stay round, and Base UI centers it on its value, so it grows on both sides (along its height in a vertical slider).
  • A press on the track glides the thumb and the indicator to the new value on the same spring: as quick as v1's switch thumb, calmer (it goes about 1.5% past the value, where v1's switch thumb went about 4%). Base UI positions them with layout: they jump to the new place, and a transform offset springs back (FLIP), so nothing animates left. With a range slider, only the thumb that moves glides.
  • Dragging follows the pointer instantly, and keyboard changes are instant. Dragging right after a press cancels the glide: the thumb sits under the pointer.
  • Under reduced motion, presses jump and nothing stretches.
  • Without a locale, Base UI formats the numbers (the Value, aria-valuetext) in the runtime's locale: the server's on the server and the browser's in the browser, so a page rendered in English and opened in another language didn't hydrate cleanly ("0.5" against "0,5", "1,000" against "1 000"). Animate UI formats them in en-US on the server and while the page hydrates, then in the browser's locale, as React Aria does. Pass locale to pick one yourself.

API Reference

Every part takes exactly the props of Base UI's Slider. Animate UI adds the animation props below.

Root, Label, Value, Control, Track

Base UI props only. Presses are tracked on the Control.

Indicator

Base UI props, plus:

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 400, damping: 32 }

Thumb

Base UI props, plus:

PropTypeDefault
transition?
Transition{ type: 'spring', stiffness: 400, damping: 32 }

The pressed thumb also gets data-pressed. The Indicator and the Thumb render through Base UI's render prop: a render of your own becomes the animated element. AnimateUIProvider's tempo scales these transitions with everything else.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/18/2026

On this page