HextaUI

Slider

Pick a value or a range by dragging, tapping the track or using the keys, with jumps that glide and an optional value bubble.

Volume
60
import { Slider, SliderLabel, SliderValue } from "@/components/ui/slider"

export function SliderDemo() {
  return (
    <Slider defaultValue={60} className="max-w-xs">
      <SliderLabel>Volume</SliderLabel>
      <SliderValue />
    </Slider>
  )
}
pnpm dlx shadcn@latest add https://hextaui.com/r/slider.json

Adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

import { Slider, SliderLabel, SliderValue } from "@/components/ui/slider"
<Slider defaultValue={[50]} max={100} step={1} aria-label="Volume" />

<Slider defaultValue={50}>
  <SliderLabel>Volume</SliderLabel>
  <SliderValue />
</Slider>

<Slider /> draws one thumb per value, so a number or a one-item array makes a single slider and two items make a range. Children such as <SliderLabel /> and <SliderValue /> sit on one line above the track.

While you drag, the thumb follows the pointer exactly. When the value jumps, from a press on the track, a key or a new controlled value, the thumb and fill glide there in 180ms instead of teleporting.

Slider
├── SliderLabel
└── SliderValue

Sizes

sm, default and lg scale the thumb and track together.

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

export function SliderSizes() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-8">
      <Slider size="sm" defaultValue={25} aria-label="Small" />
      <Slider defaultValue={50} aria-label="Default" />
      <Slider size="lg" defaultValue={75} aria-label="Large" />
    </div>
  )
}

Range

Pass two values for a range. Name each thumb with getAriaLabel, and keep them apart with minStepsBetweenValues. With draggableRange, drag anywhere between the thumbs to move both at once; a tap there still moves the nearest thumb.

Price
$200 – $800
"use client"

import { Slider, SliderLabel, SliderValue } from "@/components/ui/slider"

export function SliderRange() {
  return (
    <Slider
      defaultValue={[200, 800]}
      max={1000}
      step={10}
      minStepsBetweenValues={10}
      draggableRange
      format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
      getAriaLabel={(index) =>
        index === 0 ? "Minimum price" : "Maximum price"
      }
      className="max-w-xs"
    >
      <SliderLabel>Price</SliderLabel>
      <SliderValue />
    </Slider>
  )
}

Value bubble

showValue shows the formatted value above the thumb while it is dragged or focused from the keyboard. Use it instead of a label row, since the bubble rises into the space above the track.

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

export function SliderValueBubble() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-10 pt-8">
      <Slider
        defaultValue={0.4}
        max={1}
        step={0.01}
        format={{ style: "percent" }}
        showValue
        aria-label="Opacity"
      />
      <Slider defaultValue={[30, 70]} showValue aria-label="Range" />
    </div>
  )
}

Steps and format

step snaps the value, largeStep sets the Page Up and Shift + arrow jump, and format formats every number the slider shows or announces.

Playback speed
1.00
import { Slider, SliderLabel, SliderValue } from "@/components/ui/slider"

export function SliderSteps() {
  return (
    <Slider
      defaultValue={1}
      min={0.5}
      max={2}
      step={0.25}
      largeStep={0.5}
      format={{ minimumFractionDigits: 2 }}
      className="max-w-xs"
    >
      <SliderLabel>Playback speed</SliderLabel>
      <SliderValue />
    </Slider>
  )
}

Controlled

onValueChange fires on every move and onValueCommitted once on release, which is the moment to save. Setting the value from outside glides too.

Brightness
40
Saved: 40
"use client"

import * as React from "react"

import { Button } from "@/components/ui/button"
import { Slider, SliderLabel, SliderValue } from "@/components/ui/slider"

export function SliderControlled() {
  const [value, setValue] = React.useState(40)
  const [saved, setSaved] = React.useState(40)

  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <Slider
        value={value}
        onValueChange={setValue}
        onValueCommitted={setSaved}
      >
        <SliderLabel>Brightness</SliderLabel>
        <SliderValue />
      </Slider>
      <div className="flex items-center justify-between gap-3">
        <span className="text-sm text-muted-foreground">Saved: {saved}</span>
        <div className="flex gap-2">
          <Button variant="outline" size="sm" onClick={() => setValue(0)}>
            Off
          </Button>
          <Button variant="outline" size="sm" onClick={() => setValue(100)}>
            Max
          </Button>
        </div>
      </div>
    </div>
  )
}

Vertical

With orientation="vertical" the slider fills its parent's height, and Up raises the value.

60 Hz
250 Hz
1 kHz
4 kHz
16 kHz
import { Slider } from "@/components/ui/slider"

const bands = [
  { label: "60 Hz", value: 70 },
  { label: "250 Hz", value: 45 },
  { label: "1 kHz", value: 55 },
  { label: "4 kHz", value: 80 },
  { label: "16 kHz", value: 35 },
]

export function SliderVertical() {
  return (
    <div className="flex h-48 gap-6">
      {bands.map((band) => (
        <div key={band.label} className="flex flex-col items-center gap-3">
          <Slider
            orientation="vertical"
            defaultValue={band.value}
            aria-label={band.label}
          />
          <span className="text-xs text-muted-foreground tabular-nums">
            {band.label}
          </span>
        </div>
      ))}
    </div>
  )
}

With icons

Place icons beside the slider in a flex row. The slider takes the remaining width.

import { IconVolume, IconVolume3 } from "@tabler/icons-react"

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

export function SliderIcons() {
  return (
    <div className="flex w-full max-w-xs items-center gap-3 text-muted-foreground">
      <IconVolume3 className="size-4 shrink-0" aria-hidden />
      <Slider defaultValue={50} aria-label="Volume" />
      <IconVolume className="size-4 shrink-0" aria-hidden />
    </div>
  )
}

Disabled

The whole slider dims and ignores pointer and keyboard input.

Bass boost
30
import { Slider, SliderLabel, SliderValue } from "@/components/ui/slider"

export function SliderDisabled() {
  return (
    <Slider defaultValue={30} disabled className="max-w-xs">
      <SliderLabel>Bass boost</SliderLabel>
      <SliderValue />
    </Slider>
  )
}

Long labels

Long labels wrap, and the value stays on the end.

Annual household income before taxes, including side projects
$1,200 – $48,000
import { Slider, SliderLabel, SliderValue } from "@/components/ui/slider"

export function SliderLongLabel() {
  return (
    <Slider
      defaultValue={[1200, 48000]}
      min={0}
      max={50000}
      step={100}
      format={{ style: "currency", currency: "USD", maximumFractionDigits: 0 }}
      className="max-w-xs"
    >
      <SliderLabel>
        Annual household income before taxes, including side projects
      </SliderLabel>
      <SliderValue />
    </Slider>
  )
}

Right to left

Inside a right-to-left parent the fill starts from the right and the arrow keys follow it. Pass locale for the reader's digits.

مستوى الصوت
٦٥
النطاق
٢٠ – ٧٠
import { Slider, SliderLabel, SliderValue } from "@/components/ui/slider"

export function SliderRtl() {
  return (
    <div dir="rtl" className="flex w-full max-w-xs flex-col gap-8">
      <Slider defaultValue={65} locale="ar-EG">
        <SliderLabel>مستوى الصوت</SliderLabel>
        <SliderValue />
      </Slider>
      <Slider defaultValue={[20, 70]} locale="ar-EG" draggableRange>
        <SliderLabel>النطاق</SliderLabel>
        <SliderValue />
      </Slider>
    </div>
  )
}
KeyAction
TabMoves focus to the next thumb.
→↑Increases the value by one step. → decreases it in right-to-left.
←↓Decreases the value by one step. ← increases it in right-to-left.
Shift + ArrowPage UpPage DownMoves by largeStep.
HomeSets the minimum, or the previous thumb's value in a range.
EndSets the maximum, or the next thumb's value in a range.
  • Each thumb holds a native input type="range", so it is a slider with its own value, min and max.
  • <SliderLabel /> names every thumb. Without one, aria-label on <Slider /> is passed to the thumbs, not the wrapper, so the slider is never left unnamed. In a range, use getAriaLabel to tell the thumbs apart.
  • <SliderValue /> and the value bubble are not announced on every change; the thumb already reports its value.
  • Pressing a thumb doesn't show a focus ring; the arrow keys do. With reduced motion the thumb jumps instead of gliding.
  • On touch screens the hit area grows to at least 44px without changing the layout.

Built on the Base UI slider. Every part accepts the props of the primitive it wraps.

PropTypeDefault
valueThe controlled value. One thumb per item.
number | number[]–
defaultValue
number | number[]–
onValueChangeFires on every change while dragging.
(value, eventDetails) => void–
onValueCommittedFires once when a change ends.
(value, eventDetails) => void–
min
number0
max
number100
step
number1
largeStep
number10
minStepsBetweenValues
number0
size
"sm" | "default" | "lg""default"
orientation
"horizontal" | "vertical""horizontal"
draggableRangeIn a range, dragging between the thumbs moves them together, keeping the gap.
booleanfalse
showValueShows the value above the thumb while dragging or keyboard-focused.
booleanfalse
thumbAlignmentedge keeps the thumb inside the track at min and max.
"edge" | "center" | "edge-client-only""edge"
thumbCollisionBehavior
"push" | "swap" | "none""push"
format
Intl.NumberFormatOptions–
locale
Intl.LocalesArgument"en-US"
aria-labelNames the thumbs.
string–
getAriaLabelNames each thumb of a range.
(index: number) => string–
getAriaValueText
(formattedValue: string, value: number, index: number) => string–
name
string–
disabled
booleanfalse
className
string | (state) => string–
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="slider"The root.
data-sizesm, default or lg.
data-jumpPresent for 220ms while the thumb glides to a new value.
data-range-draggingPresent while the whole range is being dragged.
data-orientationhorizontal or vertical.
data-draggingPresent while a pointer is down.
data-disabledPresent when disabled.
data-invalidPresent when invalid inside a Field.
--slider-thumbThe thumb's diameter, set by size.
--slider-trackThe track's thickness, set by size.
data-slot="slider-control"The pressable area around the track.
data-slot="slider-track"The track.
data-slot="slider-range"The fill between the start, or the first thumb, and the last thumb.
data-slot="slider-thumb"Each thumb. data-index gives its position.
data-activeOn the thumb that is focused or being dragged.
data-slot="slider-thumb-value"The value bubble from showValue.

Names the thumbs and grows to fill the row. Renders a <div>.

PropTypeDefault
render
ReactElement | (props, state) => ReactElement<div>
AttributeDescription
data-slot="slider-label"The label.

Shows the formatted values, joined with an en dash in a range. Renders an <output>.

PropTypeDefault
childrenCustom text.
(formattedValues: string[], values: number[]) => ReactNode–
render
ReactElement | (props, state) => ReactElement<output>
AttributeDescription
data-slot="slider-value"The value.
data-orientationhorizontal or vertical.
data-draggingPresent while a pointer is down.
data-disabledPresent when disabled.
data-invalidPresent when invalid inside a Field.