# Slider

> Choose one value or a range along a bounded scale.

Source: https://www.honestui.com/docs/components/slider

```tsx
import { Slider } from "@/components/honest-ui/ui/slider"

export function SliderDemo() {
  return <Slider className="w-full max-w-64" defaultValue={50} />
}

```

## Overview [#overview]

Use a Slider when people adjust a value on a bounded scale and seeing the range helps: volume, opacity, price bounds, thresholds, percentages. Dragging to an approximate position is faster than typing when precision matters less than direction — "a bit louder", "somewhere between $20 and $40".

When the exact number is the point, use Number Field instead, or pair it with a slider so each input style covers the other's weakness. A slider with no visible value hides the one thing keyboard users cannot verify, so show the number whenever it would be read.

## Anatomy [#anatomy]

A slider has a track, a filled indicator, thumbs, an optional value display, and the scale defined by `min`, `max`, and `step`. The thumb is a ridged pill (24 × 20 px in the default `size="large"`) that scales up and lifts while dragged or pressed; `size="small"` renders a slimmer round grip for dense layouts.

The component renders the track, indicator, and one thumb per value automatically — pass two values for a range slider and it renders two thumbs without extra markup. The root also sets `data-variant="range"` or `"single"`, which styling can target.

## Behavior [#behavior]

**Values and steps.** `min` defaults to 0 and `max` to 100; `step` defaults to 1 and supports decimals. Keyboard movement snaps to multiples of `step` measured from `min`, and <kbd>Page Up</kbd>/<kbd>Page Down</kbd> or <kbd>Shift</kbd>+arrows jump by `largeStep` (10 by default). Keep `(max − min)` divisible by `step` so the ends of the track are reachable exactly.

**Range sliders.** Two thumbs bound a range. `minStepsBetweenValues` keeps them apart by a minimum distance, and `thumbCollisionBehavior` decides what happens when they meet: `push` (default) shoves the neighbor, `swap` exchanges their places, `none` blocks the move.

**Commit timing.** `onValueChange` fires continuously while dragging; `onValueCommitted` fires on release. Do expensive work — network requests, filtering large lists — on commit, not on every intermediate value.

**Disabled.** `disabled` dims the whole control to half opacity and ignores pointer events. Disabled sliders keep their last value visible, so they suit locked settings better than hidden ones.

## Accessibility [#accessibility]

Each thumb renders a hidden native range input, so assistive technology announces it as a slider with its current value, minimum, maximum, and step. Focus lands directly on the active thumb, and the focus ring draws around the thumb visual only for keyboard focus.

Full keyboard support per thumb: <kbd>Arrow</kbd> keys step by `step` (<kbd>Shift</kbd>+<kbd>Arrow</kbd> steps by `largeStep`), <kbd>Page Up</kbd>/<kbd>Page Down</kbd> step by `largeStep`, and <kbd>Home</kbd>/<kbd>End</kbd> jump to the extremes (respecting the neighboring thumb's position in ranges). In vertical orientation <kbd>Arrow Up</kbd> increases; horizontal arrows also work in both orientations.

Name the slider through a connected `Label` and pair it with `SliderValue`, which renders the formatted numbers inside a semantic `<output>` element. Without a visible value, sighted users squint at thumb positions and screen-reader users must trust the announced number alone.

The interactive area is forgiving: the control region is 28 px tall around the track and stretches the full width, so imprecise pointer presses still land on the control even though the visual thumb is smaller. Vertical sliders need an explicit height (`h-full` inside a sized container); give them at least 80 px of travel.

Colors come from `--hui-*` tokens, so the track, indicator, thumb, and value bubble adapt to dark mode automatically. The layout uses logical properties and Base UI handles right-to-left dragging, mirroring the mapping between arrow keys and direction. Long labels sit above the track and wrap normally; the floating value bubble stays on one line and never clips mid-drag.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;slider&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/slider.tsx

```tsx
"use client"

import * as React from "react"
import { Slider as SliderPrimitive } from "@base-ui-components/react/slider"

import { cn } from "@/lib/utils"

function Slider({
  className,
  children,
  defaultValue,
  value,
  min = 0,
  max = 100,
  showValue = false,
  size = "large",
  ...props
}: SliderPrimitive.Root.Props & {
  showValue?: boolean
  size?: "small" | "large"
}) {
  const _values = React.useMemo(() => {
    if (value !== undefined) {
      return Array.isArray(value) ? value : [value]
    }
    if (defaultValue !== undefined) {
      return Array.isArray(defaultValue) ? defaultValue : [defaultValue]
    }
    return [min]
  }, [value, defaultValue, min])

  return (
    <SliderPrimitive.Root
      thumbAlignment="center"
      data-size={size}
      data-variant={_values.length > 1 ? "range" : "single"}
      className={cn(
        "relative flex touch-none p-0 select-none data-disabled:opacity-50 data-[orientation=horizontal]:w-full data-[orientation=horizontal]:min-w-[var(--hui-space-15)] data-[orientation=horizontal]:flex-col data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-[var(--hui-space-15)]",
        !children &&
          "data-[orientation=horizontal]:h-[var(--hui-space-8)] data-[orientation=horizontal]:items-center data-[orientation=vertical]:w-[var(--hui-space-8)]",
        className
      )}
      defaultValue={defaultValue}
      value={value}
      min={min}
      max={max}
      {...props}
    >
      {children}
      <SliderPrimitive.Control
        data-slot="slider-control"
        className="relative flex items-center data-disabled:pointer-events-none data-[orientation=horizontal]:h-[var(--hui-space-8)] data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-[var(--hui-space-8)] data-[orientation=vertical]:flex-col"
      >
        <SliderPrimitive.Track
          data-slot="slider-track"
          className="relative grow rounded-[var(--hui-radius-full)] bg-[var(--hui-color-background-neutral-secondary)] data-[orientation=horizontal]:mx-[var(--hui-space-4)] data-[orientation=horizontal]:h-[var(--hui-space-2)] data-[orientation=vertical]:my-[var(--hui-space-4)] data-[orientation=vertical]:w-[var(--hui-space-2)]"
        >
          <SliderPrimitive.Indicator
            data-slot="slider-indicator"
            className="!absolute rounded-[var(--hui-radius-full)] bg-[var(--hui-color-background-accent-emphasis)] data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
          />
          {Array.from({ length: _values.length }, (_, index) => (
            <SliderPrimitive.Thumb
              data-slot="slider-thumb"
              data-size={size}
              index={index}
              key={index}
              className="group/slider-thumb absolute flex cursor-grab items-center justify-center outline-none active:cursor-grabbing active:outline-none has-focus-visible:[&_[data-slot=slider-thumb-visual]]:[outline:var(--hui-focus-ring)] hover:[&_[data-slot=slider-thumb-visual]]:bg-[var(--hui-color-background-base-secondary)] active:[&_[data-slot=slider-thumb-visual]]:scale-[1.08] active:[&_[data-slot=slider-thumb-visual]]:shadow-[var(--hui-shadow-lifted)] data-dragging:[&_[data-slot=slider-thumb-visual]]:scale-[1.08] data-dragging:[&_[data-slot=slider-thumb-visual]]:shadow-[var(--hui-shadow-lifted)] [&_input:focus-visible]:outline-none"
            >
              <span
                data-slot="slider-thumb-visual"
                className={cn(
                  "relative flex items-center justify-center border-[0.5px] border-[var(--hui-color-border-base-tertiary)] bg-[var(--hui-color-background-base-primary)] shadow-[var(--hui-shadow-soft)] motion-safe:[transition:transform_var(--hui-duration-press)_var(--hui-ease-out),box-shadow_var(--hui-duration-press)_var(--hui-ease-out)]",
                  size === "large"
                    ? "h-[var(--hui-space-6)] w-[var(--hui-space-7)] gap-[3px] rounded-[var(--hui-radius-2)]"
                    : "h-[var(--hui-space-5)] w-[var(--hui-space-3)] rounded-[var(--hui-radius-full)]"
                )}
              >
                {size === "large" && (
                  <>
                    <span className="h-[6px] w-px rounded-[var(--hui-radius-1)] bg-[var(--hui-color-border-base-tertiary)]" />
                    <span className="h-[6px] w-px rounded-[var(--hui-radius-1)] bg-[var(--hui-color-border-base-tertiary)]" />
                  </>
                )}
              </span>
              {showValue && (
                <SliderPrimitive.Value
                  data-slot="slider-thumb-label"
                  className={cn(
                    "absolute top-[calc(-1*(var(--hui-space-8)+1px))] left-1/2 -translate-x-1/2 whitespace-nowrap rounded-[var(--hui-radius-2)] border-[0.5px] border-[var(--hui-color-border-base-primary)] bg-[var(--hui-color-background-base-primary)] p-[var(--hui-space-2)] text-[var(--hui-color-foreground-base-primary)] shadow-[var(--hui-shadow-soft)]",
                    size === "small" && "top-[calc(-1*var(--hui-space-7))]"
                  )}
                >
                  {(formattedValues) => formattedValues[index]}
                </SliderPrimitive.Value>
              )}
            </SliderPrimitive.Thumb>
          ))}
        </SliderPrimitive.Track>
      </SliderPrimitive.Control>
    </SliderPrimitive.Root>
  )
}

function SliderValue({ className, ...props }: SliderPrimitive.Value.Props) {
  return (
    <SliderPrimitive.Value
      data-slot="slider-value"
      className={cn("flex justify-end text-sm", className)}
      {...props}
    />
  )
}

export { Slider, SliderValue }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import { Slider, SliderValue } from "@/components/ui/slider";
```

```tsx
<Slider defaultValue={50} />
```

With no children, the slider renders just the track and thumbs. Pass children to add a label row above:

```tsx
<Slider defaultValue={50}>
  <div className="mb-2 flex items-center justify-between gap-1">
    <Label className="text-sm font-medium">Opacity</Label>
    <SliderValue />
  </div>
</Slider>
```

Set `showValue` on the root instead to float a formatted value bubble above each thumb, visible whenever the slider renders.

## Don't do this [#dont-do-this]

### Choosing a precise quantity by drag [#choosing-a-precise-quantity-by-drag]

```tsx
// Bad
<Slider min={0} max={9999} defaultValue={100} />

// Good
<NumberField min={0} max={9999} defaultValue={100} />
```

Four significant digits cannot be hit reliably by dragging, and every keyboard adjustment crawls through thousands of steps. When the exact number matters more than the gesture — quantities, ports, prices — a typeable field with steppers does the job the slider cannot.

### A slider that never shows its value [#a-slider-that-never-shows-its-value]

```tsx
// Bad
<Label htmlFor={id}>Opacity</Label>
<Slider id={id} defaultValue={0.4} />

// Good
<Slider defaultValue={0.4} format={{ style: "percent" }}>
  <div className="mb-2 flex items-center justify-between gap-1">
    <Label className="text-sm font-medium">Opacity</Label>
    <SliderValue />
  </div>
</Slider>
```

Thumb position communicates relative magnitude only: nobody can tell 38% from 42% by eye. The missing value also breaks review workflows — someone returning to the form later cannot read back what was chosen. Show `SliderValue` whenever the number could be recorded, compared, or reproduced, and connect the label through `Field` when the slider needs a programmatic name.

### Doing heavy work on every pixel of drag [#doing-heavy-work-on-every-pixel-of-drag]

```tsx
// Bad
<Slider
  defaultValue={[0, 100]}
  onValueChange={(range) => refetchResults(range)}
/>

// Good
<Slider
  defaultValue={[0, 100]}
  onValueCommitted={(range) => refetchResults(range)}
/>
```

`onValueChange` fires for every intermediate value during a drag, so network calls pile up and results flash uselessly. Filter optimistically if it is cheap; otherwise wait for `onValueCommitted`, which fires once when the pointer or key interaction ends.

## Examples [#examples]

Examples cover labeled values, range selection, disabled state, vertical orientation, and form integration.

For accessible labeling and validation, use `Field` to connect the slider with its label, value, description, and error. See the [Field examples](/docs/components/field#examples).

### With Label and Value [#with-label-and-value]

```tsx
import { Label } from "@/components/honest-ui/ui/label"
import { Slider, SliderValue } from "@/components/honest-ui/ui/slider"

export function SliderWithLabelValue() {
  return (
    <Slider className="w-full max-w-64" defaultValue={50}>
      <div className="mb-2 flex items-center justify-between gap-1">
        <Label className="text-sm font-medium">Opacity</Label>
        <SliderValue />
      </div>
    </Slider>
  )
}

```

### Range Slider [#range-slider]

```tsx
import { Slider } from "@/components/honest-ui/ui/slider"

export function SliderDemo() {
  return <Slider className="w-full max-w-64" defaultValue={[25, 75]} />
}

```

### Disabled [#disabled]

A disabled slider freezes at its current value but stays readable — useful for locked settings that should remain visible.

```tsx
import { Label } from "@/components/honest-ui/ui/label"
import { Slider } from "@/components/honest-ui/ui/slider"

export function SliderDisabled() {
  return (
    <Slider
      className="w-full max-w-64"
      defaultValue={320}
      min={64}
      max={320}
      step={32}
      disabled
    >
      <div className="mb-2 flex items-center justify-between gap-1">
        <Label className="text-sm font-medium">Bitrate</Label>
        <span className="text-sm tabular-nums text-muted-foreground">
          320 kbps
        </span>
      </div>
    </Slider>
  )
}

```

### Vertical [#vertical]

```tsx
import { Slider } from "@/components/honest-ui/ui/slider"

export function SliderVertical() {
  return <Slider orientation="vertical" defaultValue={50} />
}

```

### Form Integration [#form-integration]

```tsx
"use client";

import * as React from "react";

import { Button } from "@/components/honest-ui/ui/button";
import {
  Field,
  FieldDescription,
  FieldLabel,
} from "@/components/honest-ui/ui/field";
import { Form } from "@/components/honest-ui/ui/form";
import { Slider, SliderValue } from "@/components/honest-ui/ui/slider";

export function SliderForm() {
  const [status, setStatus] = React.useState("");
  const [value, setValue] = React.useState<number | readonly number[]>([
    25, 75,
  ]);

  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const volumes = formData.getAll("volume");
    setStatus(`Submitted volume range: ${volumes.join(" to ")}.`);
  };

  return (
    <Form onSubmit={onSubmit} className="w-full max-w-64 grid gap-4">
      <Field name="volume" className="items-stretch gap-3">
        <Slider value={value} onValueChange={setValue}>
          <div className="mb-2 flex items-center justify-between gap-1">
            <FieldLabel>Volume</FieldLabel>
            <SliderValue />
          </div>
        </Slider>
        <FieldDescription>Choose a value between 0 and 100.</FieldDescription>
      </Field>
      <Button type="submit">Save volume</Button>
      <p className="text-sm text-muted-foreground" role="status">
        {status}
      </p>
    </Form>
  );
}

```

## API reference [#api-reference]

`Slider` accepts Base UI Slider root props plus Honest UI additions:

| Prop        | Values           | Default |
| ----------- | ---------------- | ------- |
| `size`      | `large`, `small` | `large` |
| `showValue` | boolean          | `false` |

Root props such as `min` (0), `max` (100), `step` (1), `largeStep` (10), `orientation`, `disabled`, `format`, `locale`, `value`/`onValueChange`, and `onValueCommitted` go on the root; `thumbAlignment` is preset to `center`. One thumb renders per value, so an array value produces a range slider automatically. `SliderValue` forwards Base UI value props and renders formatted output inside a `<output>` element.

See the [Base UI Slider API](https://base-ui.com/react/components/slider#api-reference).
