---
title: Slider
description: Range slider component with single or multiple thumbs and step markers.
sidebar:
  order: 12
search:
  tags: [range, value, thumb, step]
---

# Slider

> Range slider component with single or multiple thumbs and step markers.

## Import

```tsx
import { Slider } from 'moraine'
```

## Slot Structure

Track with a fill range and one or more draggable thumb handles.

```text
root
├── track
│   ├── range
│   └── divider (optional ×n)
└── thumb (×n)
```

## Examples

### Controlled Single

Input phase updates with onValueChange and commit phase updates with `onChange`.

```tsx
function ControlledSingle() {
  const [singleValue, setSingleValue] = createSignal(32)
  const [singleCommit, setSingleCommit] = createSignal(32)

  return (
    <div class="max-w-xl space-y-3">
      <Slider
        value={singleValue()}
        min={0}
        max={100}
        step={1}
        onValueChange={setSingleValue}
        onChange={setSingleCommit}
      />
      <p class="text-xs text-muted-foreground">Current value: {singleValue()}</p>
      <p class="text-xs text-muted-foreground">Last committed value: {singleCommit()}</p>
    </div>
  )
}
```

### Variants

Default and bold variants with visual step dividers.

> There will be no effective if `divider` set to true but `step` does not set.

```tsx
function Variants() {
  return (
    <div class="w-lg space-y-5">
      <div class="space-y-2">
        <label class="text-xs text-muted-foreground block uppercase">Default with divider</label>
        <Slider divider min={0} max={100} step={10} defaultValue={40} />
      </div>
      <div class="space-y-2">
        <label class="text-xs text-muted-foreground block uppercase">Bold with divider</label>
        <Slider divider variant="bold" min={0} max={100} step={10} defaultValue={30} />
      </div>
    </div>
  )
}
```

### Sizes

Track and thumb sizing from xs to xl.

```tsx
function Sizes() {
  const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const

  return (
    <div class="flex gap-4 w-full">
      <div class="flex flex-(1 col) gap-4">
        <For each={SIZES}>
          {(size) => (
            <div class="space-y-2">
              <label class="text-xs text-muted-foreground block uppercase">{size}</label>
              <Slider size={size} defaultValue={35} />
            </div>
          )}
        </For>
      </div>
      <div class="flex flex-(1 col) gap-4">
        <For each={SIZES}>
          {(size) => (
            <div class="space-y-2">
              <label class="text-xs text-muted-foreground block uppercase">{size}</label>
              <Slider variant="bold" size={size} defaultValue={35} />
            </div>
          )}
        </For>
      </div>
    </div>
  )
}
```

### Disabled

Disabled sliders keep values visible while preventing interaction.

```tsx
function Disabled() {
  return <Slider disabled min={0} max={100} step={10} defaultValue={35} />
}
```

### Orientations and Invert

Horizontal default layout and vertical layout with fixed container height.

```tsx
function Orientations() {
  const [horizontalValue, setHorizontalValue] = createSignal(45)
  const [verticalValue, setVerticalValue] = createSignal(45)
  const [inverted, setInverted] = createSignal(false)
  const [isBold, setIsBold] = createSignal(false)

  return (
    <div class="max-w-xl space-y-4">
      <Switch label="Invert direction" checked={inverted()} onChange={setInverted} />
      <Switch label="Bold variant" checked={isBold()} onChange={setIsBold} />
      <div class="gap-8 grid items-start sm:grid-cols-2">
        <div class="w-50 space-y-2">
          <label class="text-xs text-muted-foreground block">Horizontal: {horizontalValue()}</label>
          <Slider
            inverted={inverted()}
            variant={isBold() ? 'bold' : undefined}
            value={horizontalValue()}
            onValueChange={setHorizontalValue}
          />
        </div>
        <div class="space-y-2">
          <label class="text-xs text-muted-foreground block">Vertical: {verticalValue()}</label>
          <div class="flex h-48 items-center">
            <Slider
              orientation="vertical"
              inverted={inverted()}
              variant={isBold() ? 'bold' : undefined}
              value={verticalValue()}
              onValueChange={setVerticalValue}
            />
          </div>
        </div>
      </div>
    </div>
  )
}
```

### Range Slider

Two thumbs with controlled array value, optional minimum gap, and configurable thumb crossing.

```tsx
function RangeSlider() {
  const [rangeValue, setRangeValue] = createSignal<number[]>([20, 75])
  const [minStepsBetweenThumbs, setMinStepsBetweenThumbs] = createSignal(0)
  const [allowThumbCrossing, setAllowThumbCrossing] = createSignal(true)

  return (
    <div class="max-w-xl space-y-3">
      <Checkbox
        checked={allowThumbCrossing()}
        onChange={setAllowThumbCrossing}
        label="Allow dragging across overlapping thumbs"
      />
      <Checkbox
        checked={minStepsBetweenThumbs() > 0}
        onChange={(isChecked) => setMinStepsBetweenThumbs(isChecked ? 10 : 0)}
        label="Min steps between thumbs"
      />
      <Slider
        value={rangeValue()}
        min={0}
        max={100}
        step={1}
        minStepsBetweenThumbs={minStepsBetweenThumbs()}
        allowThumbCrossing={allowThumbCrossing()}
        onValueChange={(next) => {
          if (Array.isArray(next)) {
            setRangeValue(next)
          }
        }}
      />
      <p class="text-xs text-muted-foreground w-50">
        Range: {rangeValue()[0]} - {rangeValue()[1]}
      </p>
      <p class="text-xs text-muted-foreground w-50">
        Thumb crossing:{' '}
        {allowThumbCrossing() && minStepsBetweenThumbs() === 0 ? 'Enabled' : 'Constrained'}
      </p>
      <p class="text-xs text-muted-foreground w-50">Min steps between: {minStepsBetweenThumbs()}</p>
    </div>
  )
}
```

### Form Integration

Submit to validate required minimum value through Form + FormField.

```tsx
function FormIntegration() {
  const [formState, setFormState] = createSignal({
    volume: 10,
  })

  const updateFormVolume = (nextValue: SliderT.Value) => {
    const next = Array.isArray(nextValue) ? (nextValue[0] ?? 0) : nextValue
    setFormState((prev) => ({ ...prev, volume: next }))
  }
  const form = createForm({
    schema: v.object({ volume: v.pipe(v.number(), v.minValue(20, 'Volume must be at least 20.')) }),
    initialInput: untrack(formState),
    validate: 'input',
  })

  return (
    <Form of={form}>
      <div class="max-w-xl space-y-4">
        <FormField name="volume" label="Volume" description="Keep it at least 20.">
          <Slider value={formState().volume} onValueChange={updateFormVolume} />
        </FormField>
        <div class="flex gap-3 items-center">
          <Button type="submit" variant="secondary" size="sm">
            Validate
          </Button>
          <p class="text-xs text-muted-foreground">Current volume: {formState().volume}</p>
        </div>
      </div>
    </Form>
  )
}
```

## API Reference

### Attributes

#### `root`

Slider container that owns track, range, thumbs, and labels.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-invalid | string \| undefined | Present when the field has a validation error. |
| data-orientation | string \| undefined | Stores the rendered orientation. |
| data-readonly | string \| undefined | Present when the field is read-only. |
| data-required | string \| undefined | Present when the field is required. |

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| role | string | Defines the semantic role exposed to assistive technology. |

#### `track`

Background rail representing the full slider range.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-orientation | string \| undefined | Stores the rendered orientation. |

#### `range`

Filled segment between the start of the range and active thumb values.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-orientation | string \| undefined | Stores the rendered orientation. |

#### `divider`

Visual marker for one slider step.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-orientation | string \| undefined | Stores the rendered orientation. |

#### `thumb`

Draggable handle for one slider value.

##### Data Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-dragging | string \| undefined | Present while the related thumb or handle is being dragged. |
| data-invalid | string \| undefined | Present when the field has a validation error. |
| data-readonly | string \| undefined | Present when the field is read-only. |
| data-required | string \| undefined | Present when the field is required. |

##### ARIA Attributes

| Attribute | Type | Description |
| --- | --- | --- |
| aria-disabled | boolean \| string \| undefined | Indicates that the control is disabled. |
| aria-label | boolean \| string \| undefined | Provides an accessible label when visible text is not sufficient. |
| aria-orientation | boolean \| string \| undefined | Communicates horizontal or vertical orientation. |
| aria-readonly | boolean \| string \| undefined | Indicates that the control value cannot be changed by the user. |
| aria-required | boolean \| string \| undefined | Indicates that user input is required. |
| aria-valuemax | boolean \| string \| undefined | Defines the maximum value for range-like controls. |
| aria-valuemin | boolean \| string \| undefined | Defines the minimum value for range-like controls. |
| aria-valuenow | boolean \| string \| undefined | Defines the current numeric value for range-like controls. |
| aria-valuetext | boolean \| string \| undefined | Provides human-readable text for the current value. |
| role | string | Defines the semantic role exposed to assistive technology. |

### Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| allowThumbCrossing | boolean \| undefined | true | Whether dragging can continue across another thumb when there is no minimum gap. |
| class | ClassValue | — | Class applied to the component root or trigger element. |
| classes | SliderT.Classes \| undefined | — | — |
| defaultValue | TValue \| undefined | — | The default value of the input (uncontrolled). |
| disabled | boolean \| undefined | false | Whether the input is disabled. |
| divider | boolean \| undefined | false | Whether to show visual step dividers on the track, only applicable when `step` is defined and greater than 0. |
| id | string \| undefined | — | The ID of the input element. |
| inverted | boolean \| undefined | — | — |
| max | number \| undefined | 100 | Maximum value of the slider. |
| min | number \| undefined | 0 | Minimum value of the slider. |
| minStepsBetweenThumbs | number \| undefined | 0 | Minimum steps required between thumbs in a multi-thumb slider. |
| name | string \| undefined | — | The name of the input element, used for form submission. |
| onChange | ((value: TValue) => void) \| undefined | — | Callback when the slider selection change is committed. |
| onValueChange | ((value: TValue) => void) \| undefined | — | Callback when the slider selection changes during interaction. |
| orientation | "horizontal" \| "vertical" \| undefined | — | — |
| readOnly | boolean \| undefined | false | Whether the input is read-only. |
| ref | JSX.HTMLElementTags["div"] extends { ref?: infer Ref; } ? Ref : never \| undefined | — | — |
| required | boolean \| undefined | false | Whether the input is required. |
| size | "xs" \| "sm" \| "md" \| "lg" \| "xl" \| undefined | — | — |
| step | number \| undefined | — | Step increment between values.<br>When omitted, pointer movement is continuous. |
| style | JSX.CSSProperties \| undefined | — | — |
| styles | SliderT.Styles \| undefined | — | — |
| value | TValue \| undefined | — | The current value of the input (controlled). |
| variant | "default" \| "bold" \| undefined | — | — |

### ARIA

Accessibility attributes and roles emitted by the component markup.

| Attribute | Type | Description |
| --- | --- | --- |
| aria-disabled | boolean \| string \| undefined | Indicates that the control is disabled. |
| aria-label | boolean \| string \| undefined | Provides an accessible label when visible text is not sufficient. |
| aria-orientation | boolean \| string \| undefined | Communicates horizontal or vertical orientation. |
| aria-readonly | boolean \| string \| undefined | Indicates that the control value cannot be changed by the user. |
| aria-required | boolean \| string \| undefined | Indicates that user input is required. |
| aria-valuemax | boolean \| string \| undefined | Defines the maximum value for range-like controls. |
| aria-valuemin | boolean \| string \| undefined | Defines the minimum value for range-like controls. |
| aria-valuenow | boolean \| string \| undefined | Defines the current numeric value for range-like controls. |
| aria-valuetext | boolean \| string \| undefined | Provides human-readable text for the current value. |
| role | string | Defines the semantic role exposed to assistive technology. |

### Data Attributes

State and slot attributes exposed for styling hooks and selectors.

| Attribute | Type | Description |
| --- | --- | --- |
| data-disabled | string \| undefined | Present when the component or item is disabled. |
| data-dragging | string \| undefined | Present while the related thumb or handle is being dragged. |
| data-invalid | string \| undefined | Present when the field has a validation error. |
| data-orientation | string \| undefined | Stores the rendered orientation. |
| data-readonly | string \| undefined | Present when the field is read-only. |
| data-required | string \| undefined | Present when the field is required. |
| data-slot | string | Identifies the rendered slot for styling hooks and selectors. |
