---
title: "Slider"
description: "A single coarse, non-clinical preference set by dragging a thumb along a track, and never a clinical reading or a precise measurement."
url: "https://opsinjs.pensievelabs.org/components/slider"
source: "https://opsinjs.pensievelabs.org/components/slider.md"
section: "Components"
status: "shipped"
kind: "component"
category: "actions-and-forms"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["range input", "drag to set"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="slider"
  status="shipped"
  questions="[
  &#x22;No contrast pair has been measured: the knob against the primary fill and the muted track, and the focus ring against all three.&#x22;,
  &#x22;Nobody has heard this in a screen reader, and the thumb carries no aria-valuetext, so it speaks a raw number rather than a described position.&#x22;,
  &#x22;Forced colours and prefers-contrast: more are unhandled, and whether the thumb still reads as a distinct object on the track is untested.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="slider" />

## Installation [#installation]

<ComponentInstall name="slider" unbuilt="false" importPath="@/components/ui/slider" dependencies="[&#x22;@base-ui/react&#x22;]" />

## Usage [#usage]

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

```tsx
<Slider
  label="Screen brightness"
  value={brightness}
  onValueChange={setBrightness}
/>
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;A coarse, non-clinical preference such as a brightness level, where the exact number does not matter.&#x22;,
  &#x22;A value adjusted by feel, where sliding towards more or less beats typing a figure.&#x22;,
  &#x22;A setting with a small set of stops the reader can actually mean.&#x22;,
]"
  avoid="[
  { case: &#x22;You are capturing a clinical reading. A slider turns a value the person meant into a value the pixel chose.&#x22;, instead: &#x22;reading-input&#x22; },
  { case: &#x22;You need a precise non-clinical quantity where the digits matter.&#x22;, instead: &#x22;number-field&#x22; },
  { case: &#x22;The reader picks one of a small set of named options rather than a point on a continuum.&#x22;, instead: &#x22;segmented-control&#x22; },
]"
/>

## Anatomy [#anatomy]

<Anatomy
  name="slider"
  parts="[
  {
    name: &#x22;Slider&#x22;,
    describes: &#x22;The root. Holds value, min, max and step, and sits on neither colour axis.&#x22;,
    prop: &#x22;value&#x22;,
  },
  {
    name: &#x22;Slider.Label&#x22;,
    describes: &#x22;The visible label, wired to the thumb's hidden input so it names the control.&#x22;,
    prop: &#x22;label&#x22;,
  },
  {
    name: &#x22;Slider.Value&#x22;,
    describes: &#x22;A text readout mirroring the value, quieter than the label, with tabular figures.&#x22;,
  },
  {
    name: &#x22;Slider.Control&#x22;,
    describes: &#x22;The pointer region. Floors its height at the target minimum and blocks touch scrolling.&#x22;,
  },
  {
    name: &#x22;Slider.Track&#x22;,
    describes: &#x22;The thin muted rail with a hairline.&#x22;,
  },
  {
    name: &#x22;Slider.Indicator&#x22;,
    describes: &#x22;The filled portion, from the track start to the thumb, in the bridged primary role.&#x22;,
  },
  {
    name: &#x22;Slider.Thumb&#x22;,
    describes: &#x22;The draggable knob. A 44px hit area nesting a real range input.&#x22;,
    prop: &#x22;disabled&#x22;,
  },
]"
/>

<CompositionTree
  name="slider"
  tree="[
  {
    part: &#x22;Slider&#x22;,
    cardinality: &#x22;1&#x22;,
    children: [
      { part: &#x22;Slider.Label&#x22;, cardinality: &#x22;1&#x22; },
      { part: &#x22;Slider.Value&#x22;, cardinality: &#x22;1&#x22; },
      {
        part: &#x22;Slider.Control&#x22;,
        cardinality: &#x22;1&#x22;,
        children: [
          {
            part: &#x22;Slider.Track&#x22;,
            cardinality: &#x22;1&#x22;,
            children: [
              { part: &#x22;Slider.Indicator&#x22;, cardinality: &#x22;1&#x22; },
              { part: &#x22;Slider.Thumb&#x22;, cardinality: &#x22;1&#x22; },
            ],
          },
        ],
      },
    ],
  },
]"
/>

## Examples [#examples]

### A coarse preference [#a-coarse-preference]

A fictional notification volume from nought to a hundred, driven by `value` and
`onValueChange`.

<ComponentPreview name="slider-a-coarse-preference" kind="example" align="start" />

### With steps [#with-steps]

`min`, `max` and `step` snap the thumb to stops: a fictional map zoom running
from one to nine in twos.

<ComponentPreview name="slider-with-steps" kind="example" align="start" />

## Content guidelines [#content-guidelines]

Name the preference, not the gesture: *Screen brightness*, never *Drag to set
brightness*. The label is the accessible name and is heard before the value. Keep
units out of the readout, because a unit invites the reader to treat a rough
position as a measurement.

<DoDont>
  <DoDont.Do>
    **"Screen brightness"** with a bare nought to a hundred readout names the
    thing adjusted.
  </DoDont.Do>

  <DoDont.Dont>
    **"Blood pressure"** with a mmHg readout dresses a rough position as a
    measurement.
  </DoDont.Dont>
</DoDont>

## Accessibility [#accessibility]

This component was audited against WCAG 2.2 AA in a source pass and a rendered
pass. The audit is author-run, it is not an independent review, and clinical
review is still pending. The rendered pass was clean: no axe violations, no
target-size breach and no reflow failure. The source already handled the flagged
risks, so nothing changed here.

* 2.5.7 Dragging: the thumb wraps Base UI unmodified, so click-to-position plus
  Arrow, Home, End and Page keys give every drag a keyboard alternative.
* 4.1.2 Name, Role, Value: role, `aria-valuenow`, `min` and `max` come from the
  nested real range input, and the `Slider.Label` text is forwarded onto it so
  the name is present at render.
* 2.5.8: the thumb hit area is floored at `--opsin-target-minimum` on both axes,
  with the knob drawn smaller inside.

Two residual items. The knob's colour fade on interaction has no reduced-motion
guard, but it is a colour change and not positional motion, so any neutralisation
belongs in a shared stylesheet. The `Slider.Value` readout renders as an `output`
with a polite live region, so while dragging the value is spoken twice, a
verbosity and not a defect. Contrast pairs remain unmeasured.

<KeyboardTable
  name="slider"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Moves focus onto the thumb&#x22;,
    notes: &#x22;One tab stop, on the thumb's nested range input.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Moves focus back out of the control&#x22;,
    notes: &#x22;A single stop in reverse too.&#x22;,
  },
  {
    keys: &#x22;Arrow Right, Arrow Up&#x22;,
    action: &#x22;Increases the value by one step&#x22;,
    notes: &#x22;The `step` prop. A right-to-left layout swaps Left and Right.&#x22;,
  },
  {
    keys: &#x22;Arrow Left, Arrow Down&#x22;,
    action: &#x22;Decreases the value by one step&#x22;,
    notes: &#x22;The `step` prop, and the value stops at `min`.&#x22;,
  },
  {
    keys: &#x22;Home&#x22;,
    action: &#x22;Sets the value to the minimum&#x22;,
    notes: &#x22;Jumps the thumb to the start.&#x22;,
  },
  {
    keys: &#x22;End&#x22;,
    action: &#x22;Sets the value to the maximum&#x22;,
    notes: &#x22;Jumps the thumb to the end.&#x22;,
  },
  {
    keys: &#x22;Page Up, Page Down&#x22;,
    action: &#x22;Increases or decreases by a larger step&#x22;,
    notes: &#x22;Base UI's default of ten, not the `step` prop.&#x22;,
  },
]"
/>

<ContrastReport component="slider" />

## API reference [#api-reference]

<PropsTable name="SliderProps" />

`value` and `onValueChange` make this controlled, so the caller holds the number.
`label` is required and a missing one raises a development warning. `min`
defaults to 0, `max` to 100 and `step` to 1.

## Related [#related]

* [ReadingInput](./reading-input.mdx) is where a clinical measurement is typed,
  with the unit and the plausibility checks.
* [NumberField](./number-field.mdx) is for a precise non-clinical quantity where
  the digits matter.
* [SegmentedControl](./segmented-control.mdx) picks a named option rather than a
  value along a continuum.
