---
title: Slider
description: Single-thumb slider with value display
visibility: guest
draft: false
---

# Slider

The `<x-wirekit::slider>` component is a styled wrapper around the native `<input type="range">`. It provides accent-colored track styling, size variants, and an optional live value display while preserving full browser accessibility.

## Usage

:::preview{title="Basic Slider"}
<x-wirekit::slider style="width: 32rem; max-width: 100%" name="volume" :min="0" :max="100" :value="50" :show-value="true" />
:::

## Width

The slider fills its parent width. Constrain it via the parent element:

```blade
<div class="max-w-sm">
    <x-wirekit::slider name="volume" :min="0" :max="100" :value="50" />
</div>
```

## Size Variants

:::preview{title="Three Sizes"}
<x-wirekit::stack style="width: 32rem; max-width: 100%" gap="md">
    <x-wirekit::slider name="s1" :value="30" size="sm" :show-value="true" />
    <x-wirekit::slider name="s2" :value="50" size="md" :show-value="true" />
    <x-wirekit::slider name="s3" :value="70" size="lg" :show-value="true" />
</x-wirekit::stack>
:::

## Step Marks

Pass `marks` to render tick marks along the track. A plain list (`[0, 25, 50, 75, 100]`) draws ticks at those positions; a value-to-label map (`[0 => 'Min', 100 => 'Max']`) adds labels underneath.

:::preview{title="Slider with labeled marks"}
<x-wirekit::slider style="width: 32rem; max-width: 100%" name="quality" :min="0" :max="100" :value="50" :marks="[0 => 'Low', 50 => 'Mid', 100 => 'High']" />
:::

### Marks that carry a meaning

A third shape gives a mark a `description` as well as a label — for the case where the positions **mean** something and the label alone cannot say what:

:::preview{title="Marks with descriptions"}
<x-wirekit::slider
    style="width: 32rem; max-width: 100%"
    name="spread"
    id="spread"
    :min="-2"
    :max="2"
    :step="1"
    :value="0"
    :marks="[
        -2 => ['label' => '−2', 'description' => 'Single verdict'],
        -1 => ['label' => '−1', 'description' => 'Lean consensus'],
        0 => ['label' => '0', 'description' => 'Neutral'],
        1 => ['label' => '+1', 'description' => 'Invite dissent'],
        2 => ['label' => '+2', 'description' => 'Full spread'],
    ]"
/>
:::

Hover a tick to read its meaning. This exists because the alternative is worse than it sounds: without it, a reader who wants to know what a position means has to **move the slider to find out** — changing the very thing they were still deciding about.

The description reaches a reader two ways, and both are needed. It becomes the tick's `title`, which is a pointer affordance — and there is no hover on touch, so `title` alone would hide the meaning from the readers most likely to be guessing at it. So it is **also what the slider announces**: as you move through the positions, a screen reader reads the description of the one you are on rather than the bare number. One position at a time, not every mark at once.

**It also becomes what the slider announces.** Where a mark has both, the description wins over the label: the label is what you see on the tick (`−2`), the description is what the position means (`Single verdict`), and the second is the useful one to hear. `valueTextMap` still overrides everything if you want to decouple the two entirely.

Both older shapes are untouched — a list and a label map render exactly as before, with no `title` and no description. The new shape is opt-in per mark, and a mark may carry a description with no label at all.

## Value Tooltip

Set `tooltip` to float a value bubble above the thumb that follows it as the user drags — handy when there's no room for a persistent value display.

:::preview{title="Slider with a value tooltip"}
<x-wirekit::slider style="width: 32rem; max-width: 100%" name="brightness" :min="0" :max="100" :value="65" tooltip />
:::

## Custom Step

```blade
<x-wirekit::slider name="price" :min="0" :max="1000" :step="25" :value="250" :show-value="true" />
```

## Livewire Integration

`<x-wirekit::slider>` keeps its displayed value in internal state seeded from the
`value` prop, so when binding with Livewire pass the bound property as `:value`
**alongside** `wire:model` to seed the initial display:

```blade
<x-wirekit::slider wire:model.live="volume" :value="$volume" min="0" max="100" />
```

Without `:value` the slider sits at `min` until the user interacts; `wire:model`
keeps it in sync afterward. This is the framework-agnostic seeding pattern
WireKit's stateful controls share — it works in plain Blade forms too.

## Optimistic UI

Pass the name of the Livewire method the slider should call and the new value is shown straight away, while it goes:

```blade
<x-wirekit::slider
    name="volume"
    label="Volume"
    :value="$volume"
    optimistic="saveVolume"
/>
```

Load `wirekit-optimistic.js` alongside whichever bundle you already use — below it, in your layout:

```blade
@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}"></script>
```

### Try it

The demo below runs the real path: the change shows immediately, the outline says it is
provisional, and the server's answer either confirms it silently or takes it back.

:::preview{title="Optimistic slider — accepted, refused, and a slow answer"}
<livewire:demos.optimistic-host>
<x-wirekit::stack gap="lg" style="max-width: 26rem;">
    <x-wirekit::slider name="opt-accept" label="Accepted — the confirmation is silent" :value="40" optimistic="demoAccept" />
    <x-wirekit::slider name="opt-reject" label="Refused — the old value comes back, and the refusal is spoken" :value="40" optimistic="demoReject" />
    <x-wirekit::slider name="opt-slow" label="Slow answer — the dashed outline is the provisional state" :value="40" optimistic="demoSlow" />
</x-wirekit::stack>
</livewire:demos.optimistic-host>
:::

:::source{language="blade"}
{{-- In your app there is no host: your own Livewire component owns the method. --}}
<x-wirekit::slider
    name="volume"
    label="Volume"
    :value="$volume"
    optimistic="saveVolume"
/>
:::

The `<livewire:demos.…>` wrapper above exists only on this site — it supplies the demo
methods so the page can show a real round trip. The block under it is what you write.

**It sends when the gesture ends, not while it runs.** Dragging produces a value per frame, and sending each one would be a hundred requests for a single decision. So the value goes once, when you let go — and immediately on an arrow key, because one keypress is already a finished decision. There is no settle delay anywhere in this: a timer would make the same gesture behave differently on a fast machine than on a slow one.

**A refusal puts the thumb back where the gesture started** — not where you released it. Both the announced value and the control itself return, so nothing is left showing a position the server declined.

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `name` | `string\|null` | `null` | Form field name |
| `error` | `string\|null` | `null` | Validation message. Rendered below the control, announced politely, and wired with `aria-invalid` + `aria-describedby` |
| `hint` | `string\|null` | `null` | Helper text below the control. An `error` replaces it — one message region, so the two cannot stack |
| `id` | `string\|null` | auto-generated | Element id |
| `min` | `int` | `0` | Minimum value |
| `max` | `int` | `100` | Maximum value |
| `step` | `int\|float` | `1` | Increment step |
| `value` | `int\|null` | `min` | Initial value |
| `size` | `string` | `'md'` | `'sm'`, `'md'`, `'lg'` |
| `showValue` | `bool` | `false` | Show live value next to slider |
| `marks` | `array` | `[]` | Tick marks — a list of positions (`[0, 50, 100]`), a position-to-label map (`[0 => 'Min', 100 => 'Max']`), or a map to `['label' => …, 'description' => …]` for positions whose meaning the label cannot carry. See [Step Marks](#step-marks). |
| `valueTextMap` | `array\|null` | `null` | A `value => spoken-text` map for `aria-valuetext`, decoupled from the visual ticks — show numeric ticks but announce semantic meaning (`[1 => 'Low', 5 => 'High']`). A caller `aria-valuetext` binding wins over both |
| `tooltip` | `bool` | `false` | Float a value bubble above the thumb that follows it |
| `disabled` | `bool` | `false` | Disabled state |
| `optimistic` | `string\|null` | `null` | Livewire method to call when the gesture ends, showing the change before the server answers |
| `optimisticArgs` | `array` | `[]` | Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to. |
| `scope` | `string\|null` | `null` | Scoped personalization key |

## Accessibility

- Wraps the native `<input type="range">` — inherits all browser a11y features
- Arrow keys move by `step`, Home/End jump to min/max, PageUp/PageDown move by larger increments
- Live value display uses `aria-live="polite"` for non-intrusive AT announcements
- Disabled state via native `disabled` attribute

## Keyboard Interaction

| Key | Action |
|-----|--------|
| `Tab` | Move focus to the slider thumb |
| `ArrowLeft` / `ArrowDown` | Decrease the value by `step` |
| `ArrowRight` / `ArrowUp` | Increase the value by `step` |
| `PageDown` | Decrease by a larger amount (typically 10× step) |
| `PageUp` | Increase by a larger amount |
| `Home` | Jump to `min` |
| `End` | Jump to `max` |

## Pitfalls

- **Don't use a slider for precise numeric input.** Use `<x-wirekit::number-input>` with a stepper. Sliders excel at fuzzy / relative values (volume, opacity) where exact numbers don't matter.
- **Don't omit a numeric label.** Touch users can't see the thumb's exact position — pair the slider with a live-updating value display.
- **A dragged slider with `wire:model.live` floods the server.** The native range input fires a change on every step during a drag, so `.live` sends one Livewire round-trip per step. Use `wire:model.live.debounce.300ms` to coalesce them, or `wire:model.lazy` to sync only when the drag ends.

## Design Tokens

The slider relies primarily on the native `<input type="range">` rendering for the track and thumb, which inherits the browser-default accent color. The wrapper around the value display uses these tokens:

| Token | Used for |
| --- | --- |
| `--text-wk-sm` | Live value font size |
| `--color-wk-text` | Live value text |
| `--padding-wk-x-sm` | Live value pill padding |
| `--opacity-wk-disabled` | Disabled visual weight |

## Config Defaults

The defaults live in `config/wirekit.php` under `components.slider`. Override them globally:

```php
'components' => [
    'slider' => ['min' => 0, 'max' => 100, 'step' => 1, 'size' => 'md'],
],
```

## Further Reading

- [MDN: `<input type="range">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/range)
- [WAI-ARIA Slider Pattern](https://www.w3.org/WAI/ARIA/apg/patterns/slider/)
- [MDN: `aria-live`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Attributes/aria-live)
- [Styling Range Inputs with CSS](https://css-tricks.com/sliding-nightmare-understanding-range-input/)
