# Slider

An input where the user selects a value from within a given range.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Slider } from "@/components/ui/slider"

const styles = stylex.create({
  slider: { maxWidth: "20rem" },
})

export default function SliderDemo() {
  return <Slider defaultValue={[75]} max={100} step={1} xstyle={styles.slider} />
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` and
   `state-styles.ts` from the same page. The slider imports both. The examples
   caption the slider with [Field](https://carbon.style/docs/components/field).

   Paste `field-state.ts` from the same installation page.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/slider.tsx"
   "use client"

   import type { CSSProperties } from "react"
   import { use, useState } from "react"
   import { Slider as SliderPrimitive } from "@base-ui/react/slider"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"

   import { stateStyles } from "@/lib/state-styles"
   import { FieldStateContext } from "@/lib/field-state"
   import { states } from "@/theme/states"
   import { color, duration, ease, radius, shadow, space } from "@/theme/tokens.stylex"

   const thumbEdge = "oklch(0 0 0 / 8%)"
   const sliderThumbBg = `light-dark(${color.background}, ${color.foreground})`

   const styles = stylex.create({
     root: {
       width: "100%",
     },
     rootVertical: {
       width: null,
       height: "100%",
     },
     control: {
       position: "relative",
       display: "flex",
       alignItems: "center",
       width: "100%",
       touchAction: "none",
       userSelect: "none",
     },
     controlVertical: {
       flexDirection: "column",
       width: "auto",
       height: "100%",
       minHeight: "10rem",
     },
     track: {
       position: "relative",
       flexGrow: 1,
       width: "100%",
       height: space.s1,
       overflow: "hidden",
       borderRadius: radius.full,
       backgroundColor: color.muted,
       boxShadow: shadow.groove,
     },
     trackVertical: {
       width: space.s1,
       height: "100%",
     },
     range: {
       backgroundColor: color.primary,
     },
     thumb: {
       width: space.s3,
       height: space.s3,
       borderRadius: radius.full,
       borderWidth: 1,
       borderStyle: "solid",
       borderColor: thumbEdge,
       backgroundColor: sliderThumbBg,
       backgroundImage: `linear-gradient(to bottom, transparent, ${color.thumbShade})`,
       backgroundOrigin: "border-box",
       outlineStyle: "none",
       boxShadow: {
         default: shadow.thumb,
         ":hover": {
           default: shadow.thumb,
           "@media (hover: hover)": `${shadow.thumb}, ${shadow.ring}`,
         },
       },
       transitionProperty: "box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
       "::after": {
         content: '""',
         position: "absolute",
         inset: `calc(-1 * ${space.s2})`,
       },
     },
     thumbRing: { borderColor: color.ring, boxShadow: `${shadow.thumb}, ${shadow.ring}` },
   })

   const sliderStyles = {
     root: styles.root,
     rootVertical: styles.rootVertical,
     control: styles.control,
     controlVertical: styles.controlVertical,
     disabled: states.disabled,
     track: styles.track,
     trackVertical: styles.trackVertical,
     range: styles.range,
     thumb: styles.thumb,
     thumbRing: styles.thumbRing,
   }

   type StateOverrides = { xstyle?: StyleXStyles; style?: CSSProperties; className?: string }

   type SliderProps<Value extends number | readonly number[]> = Omit<
     SliderPrimitive.Root.Props<Value>,
     "className" | "style"
   > &
     StateOverrides

   function Slider<Value extends number | readonly number[]>({
     xstyle,
     style,
     className,
     value,
     defaultValue,
     ...props
   }: SliderProps<Value>) {
     const count = Array.isArray(value)
       ? value.length
       : Array.isArray(defaultValue)
         ? defaultValue.length
         : 1

     return (
       <SliderPrimitive.Root
         data-slot="slider"
         value={value}
         defaultValue={defaultValue}
         thumbAlignment="edge"
         {...props}
         {...stateStyles(
           (state) => [styles.root, state.orientation === "vertical" && styles.rootVertical, xstyle],
           { className, style },
         )}
       >
         <SliderPrimitive.Control
           data-slot="slider-control"
           {...stateStyles((state) => [
             styles.control,
             state.orientation === "vertical" && styles.controlVertical,
             state.disabled && states.disabled,
           ])}
         >
           <SliderPrimitive.Track
             data-slot="slider-track"
             {...stateStyles((state) => [
               styles.track,
               state.orientation === "vertical" && styles.trackVertical,
             ])}
           >
             <SliderPrimitive.Indicator data-slot="slider-range" {...stylex.props(styles.range)} />
           </SliderPrimitive.Track>
           {Array.from({ length: count }, (_, index) => (
             <SliderThumb
               key={index}
               index={index}
               invalid={props["aria-invalid"] === true || props["aria-invalid"] === "true"}
             />
           ))}
         </SliderPrimitive.Control>
       </SliderPrimitive.Root>
     )
   }

   function SliderThumb({ index, invalid }: { index: number; invalid: boolean }) {
     const fieldState = use(FieldStateContext)
     const [focusVisible, setFocusVisible] = useState(false)

     return (
       <SliderPrimitive.Thumb
         data-slot="slider-thumb"
         data-focus-visible={focusVisible ? "" : undefined}
         index={index}
         onFocus={(event) => setFocusVisible(event.target.matches(":focus-visible"))}
         onBlur={() => setFocusVisible(false)}
         onKeyDown={() => setFocusVisible(true)}
         {...stateStyles((state) => [
           styles.thumb,
           (state.valid === false || fieldState?.valid === false || invalid) && states.invalidRing,
           (focusVisible || (state.dragging && state.activeThumbIndex === index)) && styles.thumbRing,
         ])}
       />
     )
   }

   export { Slider, sliderStyles }
   ```

4. Update the import paths to match your project setup.

## Usage

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

```tsx
<Slider defaultValue={[33]} max={100} step={1} />
```

`Slider` renders the control, the track, the filled range and the thumbs
itself. It fills its container, so set the width on the slider or its parent.
`value` and `defaultValue` take a number for one thumb or an array for one
thumb per entry.

```text
Slider
└── control
    ├── track
    │   └── range
    └── thumb
```

Inside `Field`, use `FieldLabel` for automatic association. For a bare slider, pass `aria-labelledby`. Base UI copies it to the
`<input>` inside every thumb, and a screen reader announces that input. A
label's `htmlFor` cannot reach an input the slider renders itself.

```tsx
<Label id="volume">Volume</Label>
<Slider aria-labelledby="volume" defaultValue={[40]} />
```

## Range

Use an array with two values for a range slider.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Slider } from "@/components/ui/slider"

const styles = stylex.create({
  slider: { maxWidth: "20rem" },
})

export default function SliderRange() {
  return <Slider defaultValue={[25, 50]} max={100} step={5} xstyle={styles.slider} />
}
```

## Multiple thumbs

Use an array with more than two values for a thumb per value. Thumbs push each
other when they meet; set `thumbCollisionBehavior` to `"swap"` or `"none"` to
change that.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Slider } from "@/components/ui/slider"

const styles = stylex.create({
  slider: { maxWidth: "20rem" },
})

export default function SliderMultiple() {
  return <Slider defaultValue={[10, 20, 70]} max={100} step={10} xstyle={styles.slider} />
}
```

## Vertical

Use `orientation="vertical"` for a vertical slider. It takes the height of its
parent, with a minimum of `10rem` when the parent has no height.

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

export default function SliderVertical() {
  return (
    <HStack gap={6} align="center" justify="center">
      <Slider defaultValue={[50]} max={100} step={1} orientation="vertical" />
      <Slider defaultValue={[25]} max={100} step={1} orientation="vertical" />
    </HStack>
  )
}
```

## Controlled

Pass `value` and `onValueChange` to control the slider. `onValueCommitted`
fires once when a drag or a key press ends. Use it for work that costs more
than a state update.

```tsx
"use client"

import { useState } from "react"
import * as stylex from "@stylexjs/stylex"
import { Field, FieldLabel } from "@/components/ui/field"
import { Slider } from "@/components/ui/slider"
import { HStack } from "@/components/ui/stack"
import { color, leading, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  panel: { width: "100%", maxWidth: "20rem" },
  value: {
    color: color.mutedForeground,
    fontSize: text.sm,
    lineHeight: leading.sm,
    fontVariantNumeric: "tabular-nums",
  },
})

export default function SliderControlled() {
  const [value, setValue] = useState([0.3, 0.7])

  return (
    <Field xstyle={styles.panel}>
      <HStack gap={2} align="center" justify="between">
        <FieldLabel>Temperature</FieldLabel>
        <span {...stylex.props(styles.value)}>{value.join(", ")}</span>
      </HStack>
      <Slider value={value} onValueChange={setValue} min={0} max={1} step={0.1} />
    </Field>
  )
}
```

## Disabled

Set `disabled` on `Field` to disable its control and dim its label, or on a bare control directly.

```tsx
import { VStack } from "@/components/ui/stack"
import { Field, FieldLabel } from "@/components/ui/field"
import * as stylex from "@stylexjs/stylex"
import { Slider } from "@/components/ui/slider"

const styles = stylex.create({
  slider: { maxWidth: "20rem" },
})

function BareDisabled() {
  return <Slider defaultValue={[50]} max={100} step={1} disabled xstyle={styles.slider} />
}

export default function DisabledExample() {
  return (
    <VStack gap={5}>
      <BareDisabled />
      <Field disabled>
        <FieldLabel>Volume</FieldLabel>
        <Slider defaultValue={50} />
      </Field>
    </VStack>
  )
}
```

## Invalid

Set `invalid` on `Field` or `aria-invalid` on a bare slider.

```tsx
import { Slider } from "@/components/ui/slider"
import { Field, FieldLabel, FieldError } from "@/components/ui/field"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"

export default function SliderInvalid() {
  return (
    <VStack gap={5} width="100%" maxWidth="24rem">
      <VStack gap={2}>
        <Label id="slider-invalid-bare">Volume</Label>
        <Slider
          aria-labelledby="slider-invalid-bare"
          defaultValue={25}
          aria-invalid
          aria-describedby="slider-invalid-bare-error"
        />
        <FieldError id="slider-invalid-bare-error">Choose a volume.</FieldError>
      </VStack>
      <Field invalid>
        <FieldLabel>Volume</FieldLabel>
        <Slider defaultValue={25} />
        <FieldError>Choose a volume.</FieldError>
      </Field>
    </VStack>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

`Slider` wraps the Base UI `Slider.Root` and accepts its props. See the
[Base UI reference](https://base-ui.com/react/components/slider). It also
takes `xstyle`, `style` and `className` and applies them to the root.

### Slider

| Prop                     | Type                                       | Default        |
| ------------------------ | ------------------------------------------ | -------------- |
| `value`                  | `number \| readonly number[]`              | —              |
| `defaultValue`           | `number \| readonly number[]`              | `min`          |
| `onValueChange`          | `(value, eventDetails) => void`            | —              |
| `onValueCommitted`       | `(value, eventDetails) => void`            | —              |
| `min`                    | `number`                                   | `0`            |
| `max`                    | `number`                                   | `100`          |
| `step`                   | `number`                                   | `1`            |
| `largeStep`              | `number`                                   | `10`           |
| `minStepsBetweenValues`  | `number`                                   | `0`            |
| `orientation`            | `"horizontal" \| "vertical"`               | `"horizontal"` |
| `disabled`               | `boolean`                                  | `false`        |
| `name`                   | `string`                                   | —              |
| `thumbCollisionBehavior` | `"push" \| "swap" \| "none"`               | `"push"`       |
| `thumbAlignment`         | `"center" \| "edge" \| "edge-client-only"` | `"edge"`       |
| `format`                 | `Intl.NumberFormatOptions`                 | —              |
| `locale`                 | `Intl.LocalesArgument`                     | —              |
| `xstyle`                 | `StyleXStyles`                             | —              |
| `style`                  | `CSSProperties`                            | —              |
| `className`              | `string`                                   | —              |

The root is a `<div role="group">`. Each thumb wraps an `<input type="range">`,
so the arrow keys, Home, End, Page Up and Page Down move it, Shift with an
arrow moves by `largeStep`, and a `name` submits every value with a form.
`format` and `locale` format the `aria-valuetext` a screen reader reads out.

### sliderStyles

The style maps behind the component. Spread them onto any element.

| Key               | What it is                                          |
| ----------------- | --------------------------------------------------- |
| `root`            | The full-width root                                 |
| `rootVertical`    | The full-height root                                |
| `control`         | The gesture surface: flex row, no touch scrolling   |
| `controlVertical` | The column, with its `10rem` floor                  |
| `disabled`        | The dimmed, non-interactive state                   |
| `track`           | The groove: height, fill, inset shadow, clipping    |
| `trackVertical`   | The groove turned on its side                       |
| `range`           | The primary fill                                    |
| `thumb`           | The raised circle: edge, lit face, lift, hover ring |
| `thumbRing`       | The ring while focused from the keyboard or dragged |

`disabled` is the shared `states.disabled` from `theme/states.ts`. `thumbRing`
is the shared ring with no pseudo-class on it, with the thumb's lift kept under
the halo. A thumb needs a ring without a pseudo-class because focus goes to the
`<input>` nested inside it. The hover halo stays on `thumb`, since the thumb
itself receives the hover.

### Data attributes

| Attribute                                              | On          | Effect                                         |
| ------------------------------------------------------ | ----------- | ---------------------------------------------- |
| `data-slot="slider"`                                   | the root    | Targets the slider from a parent               |
| `data-slot="slider-control"`                           | the control | Targets the gesture surface                    |
| `data-slot="slider-track"`, `data-slot="slider-range"` | the groove  | Targets the track and its fill                 |
| `data-slot="slider-thumb"`                             | a thumb     | Targets the circle                             |
| `data-orientation`, `data-disabled`, `data-dragging`   | every part  | Base UI writes them                            |
| `data-index`                                           | a thumb     | Base UI writes the thumb's position in `value` |
| `data-focus-visible`                                   | a thumb     | Set while the ring shows for keyboard focus    |
