---
title: Slider
description: Choose a number or range by moving one or more thumbs.
package: moraine
version: 0.6.0
repository: https://github.com/subframe7536/moraine
---

# Slider

> Choose a number or range by moving one or more thumbs.

Use Slider when a numeric value or range is best chosen spatially. Pair it with a labeled numeric field when users need precise text entry.

## Basic usage

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

export function Example() {
  return (
    <Field label="Volume">
      <Slider defaultValue={50} />
    </Field>
  )
}
```

## Anatomy

```text
Slider [component; slot=root]
├── track [slot]
│   ├── range [slot]
│   └── marker [slot]
└── thumb [slot]
```

## Usage

### Single values and ranges

Use a scalar value for a single thumb and an ordered pair for a range. Pair `value` with `onValueChange` for controlled state; `onValueChange` also reports immediate updates during drag when the slider owns its value.

```tsx
import { Slider } from 'moraine'
import { createSignal } from 'solid-js'

export function Values() {
  const [singleVal, setSingleVal] = createSignal(40)
  const [rangeVal, setRangeVal] = createSignal<[number, number]>([20, 80])

  return (
    <div class="max-w-md w-full space-y-6">
      <div class="space-y-2">
        <div class="text-xs text-muted-foreground flex justify-between">
          <span>Single volume</span>
          <span class="text-foreground font-mono">{singleVal()}%</span>
        </div>
        <Slider value={singleVal()} onValueChange={setSingleVal} />
      </div>

      <div class="space-y-2">
        <div class="text-xs text-muted-foreground flex justify-between">
          <span>Budget range</span>
          <span class="text-foreground font-mono">
            ${rangeVal()[0]} - ${rangeVal()[1]}
          </span>
        </div>
        <Slider value={rangeVal()} onValueChange={setRangeVal} />
      </div>
    </div>
  )
}
```

### Step and markers

Set `step` to snap pointer drag and keyboard navigation to fixed increments. Enable `marker` to render visual tick marks at each step interval across the track rail.

```tsx
import { Slider } from 'moraine'
import { createSignal } from 'solid-js'

export function StepMarker() {
  const [value, setValue] = createSignal(40)
  const [range, setRange] = createSignal([20, 60])

  return (
    <div class="max-w-md w-full space-y-6">
      <div class="space-y-2">
        <div class="text-xs text-muted-foreground flex justify-between">
          <span>Stepped single (step = 10)</span>
          <span class="text-foreground font-mono">{value()}</span>
        </div>
        <Slider
          value={value()}
          min={0}
          max={100}
          step={10}
          marker
          onValueChange={(val) => {
            if (typeof val === 'number') {
              setValue(val)
            }
          }}
        />
      </div>

      <div class="space-y-2">
        <div class="text-xs text-muted-foreground flex justify-between">
          <span>Stepped range with bold track (step = 20)</span>
          <span class="text-foreground font-mono">
            {range()[0]} - {range()[1]}
          </span>
        </div>
        <Slider
          defaultValue={[20, 60]}
          value={range()}
          min={0}
          max={100}
          step={20}
          marker
          variant="bold"
          onValueChange={(val) => {
            if (Array.isArray(val)) {
              setRange(val)
            }
          }}
        />
      </div>
    </div>
  )
}
```

### Keyboard and forms

Add `name` for native form submission, or place the slider inside `form.Field` from
[createForm](https://moraine.subf.dev/components/form.md) for schema binding. Use standalone [Field](https://moraine.subf.dev/components/field.md) when
you only need a label and messages.

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

export function KeyboardForms() {
  return (
    <div class="max-w-md w-full">
      <Slider name="brightness" defaultValue={75} step={5} min={0} max={100} />
    </div>
  )
}
```

### Keyboard interaction

When `step` is omitted, keyboard stepping defaults to 1% of the configured range. Page-sized movement is approximately 10% of the range and is snapped to `step` when one is defined.

| Key                                         | Description                                             |
| ------------------------------------------- | ------------------------------------------------------- |
| <kbd>ArrowRight</kbd> / <kbd>ArrowUp</kbd>  | Moves in the increasing direction by one keyboard step. |
| <kbd>ArrowLeft</kbd> / <kbd>ArrowDown</kbd> | Moves in the decreasing direction by one keyboard step. |
| <kbd>Shift + Arrow</kbd>                    | Moves by the page-sized increment.                      |
| <kbd>PageUp</kbd> / <kbd>PageDown</kbd>     | Moves by the page-sized increment.                      |
| <kbd>Home</kbd>                             | Moves the focused thumb to its minimum permitted value. |
| <kbd>End</kbd>                              | Moves the focused thumb to its maximum permitted value. |

## Examples

### Controlled value

```tsx
import { Button, Icon, Slider } from 'moraine'
import { createSignal } from 'solid-js'

export function ControlledSingle() {
  const [volume, setVolume] = createSignal(75)

  const volumeIcon = () => {
    if (volume() === 0) {
      return 'i-lucide:volume-x'
    }
    if (volume() < 50) {
      return 'i-lucide:volume-1'
    }
    return 'i-lucide:volume-2'
  }

  return (
    <div class="p-4 b-1 b-border rounded-xl max-w-md w-full space-y-2">
      <div class="flex items-center justify-between">
        <div class="flex gap-2 items-center">
          <Button
            variant="ghost"
            size="sm"
            class="p-0 size-8"
            onClick={() => setVolume((v) => (v === 0 ? 75 : 0))}
            aria-label="Mute / Unmute"
          >
            <Icon name={volumeIcon()} class="size-4" />
          </Button>
          <span class="text-sm font-medium">Output Volume</span>
        </div>
        <span class="text-xs text-primary font-mono font-semibold">{volume()}%</span>
      </div>

      <Slider value={volume()} min={0} max={100} step={1} onValueChange={setVolume} class="mb-2" />
    </div>
  )
}
```

### Range

```tsx
import { Checkbox, Slider } from 'moraine'
import { createSignal } from 'solid-js'

export 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()}
        onCheckedChange={setAllowThumbCrossing}
        label="Allow dragging across overlapping thumbs"
      />
      <Checkbox
        checked={minStepsBetweenThumbs() > 0}
        onCheckedChange={(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

```tsx
import { Button, createForm, Slider } from 'moraine'
import type { SliderT } from 'moraine'
import { createSignal, untrack } from 'solid-js'
import * as v from 'valibot'

export 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.Form>
      <div class="max-w-xl space-y-4">
        <form.Field name="volume" label="Volume" description="Keep it at least 20.">
          <Slider value={formState().volume} onValueChange={updateFormVolume} />
        </form.Field>
        <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.Form>
  )
}
```

### Blur slider

```tsx
import { Slider } from 'moraine'
import { createSignal } from 'solid-js'

export function BlurSlider() {
  const [value, setValue] = createSignal(32)

  return (
    <div class="py-6 flex w-full justify-center">
      <Slider
        variant="bold"
        value={value()}
        onValueChange={(val: number | number[]) => {
          if (typeof val === 'number') {
            setValue(val)
          }
        }}
        min={0}
        max={100}
        step={1}
        aria-label="Blur"
        class="h-12 max-w-md w-full isolate"
        style={{ '--s-size': '48px', '--s-len': '28px', '--s-offset': '4px' }}
        classes={{
          track: 'rounded-xl',
          thumb: 'rounded-xl',
        }}
      >
        <div class="text-sm font-semibold px-5 flex pointer-events-none items-center inset-0 justify-between absolute">
          <span class="text-white mix-blend-difference">Blur</span>
          <span class="text-white font-mono mix-blend-difference">{Math.round(value())}px</span>
        </div>
      </Slider>
    </div>
  )
}
```

## Attributes

| Attributes | Slot | Description |
| --- | --- | --- |
| `data-disabled` | `slider`, `slider-thumb` | Present when the component, slot, or item is disabled. |
| `data-dragging` | `slider`, `slider-thumb` | Present while the related thumb or handle is being dragged. |
| `data-invalid` | `slider`, `slider-thumb` | Present when the field or form has a validation error. |
| `data-inverted` | `slider`, `slider-range`, `slider-thumb` | Present when the component direction or range is inverted. |
| `data-multiple` | `slider`, `slider-range` | Present when the control contains or selects multiple values. |
| `data-readonly` | `slider`, `slider-thumb` | Present when the field is in read-only mode. |
| `data-required` | `slider`, `slider-thumb` | Present when the field input is required. |

## Props

Props for the Slider component.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| allowThumbCrossing | boolean \| undefined | true | Whether dragging can continue across another thumb when there is no minimum gap. |
| defaultValue | TValue \| undefined | — | The default value of the input (uncontrolled). |
| disabled | boolean \| undefined | false | Whether the input is disabled. |
| id | string \| undefined | — | The ID of the input element. |
| inputRef | Ref<HTMLInputElement> \| undefined | — | Optional inner input element ref. |
| inverted | boolean \| undefined | false | Reverses value movement along the track. |
| marker | boolean \| undefined | false | Whether to show visual step markers on the track, only applicable when `step` is defined and greater than 0. |
| 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. |
| onValueChange | ((value: TValue) => void) \| undefined | — | Callback when the slider selection changes during interaction. |
| onValueCommit | ((value: TValue) => void) \| undefined | — | Callback when the slider selection change is committed. |
| orientation | 'horizontal' \| 'vertical' \| undefined | 'horizontal' | Direction of keyboard and pointer value movement. |
| readOnly | boolean \| undefined | false | Whether the input is read-only. |
| required | boolean \| undefined | false | Whether the input is required. |
| size | 'sm' \| 'md' \| 'lg' \| undefined | 'md' | Visual size of the component. |
| step | number \| undefined | — | Step increment between values.<br>When omitted, pointer movement is continuous. |
| value | TValue \| undefined | — | The current value of the input (controlled). |
| variant | 'default' \| 'bold' \| undefined | 'default' | Visual treatment of the component. |
| class | SlotClassValue \| undefined | — | Class applied to the component root or trigger element. |
| classes | Classes \| undefined | — | Family slot class defaults for this instance. |
| style | SlotStyleValue \| undefined | — | Style applied to the component root or trigger element. |
| styles | Styles \| undefined | — | Family slot style defaults for this instance. |
