Skip to main content
Copy for LLM

Range Slider

The <x-wirekit::range-slider> component provides a dual-handle slider for selecting a numeric range. Both handles are draggable via pointer and keyboard, and the filled track between them visualizes the selected range. A click or tap on the track brings the nearer handle to that point.

Basic Usage

Price Range
0 1000
Range: 200 to 800

With Step

Custom Step
0 100
Range: 20 to 60

Adjust in 5 kg increments

With Hint

Hint Text
18 65
Range: 25 to 45

Filter candidates by age

Without Value Bubbles

Set :show-values="false" to hide the floating numeric badges above each handle. The min/max bounds under the track and the aria-live region for screen readers stay in place — only the tooltip-style bubbles are removed.

No Bubbles
0 1000
Range: 200 to 800

To hide the bubbles globally for every <x-wirekit::range-slider> in your app, set the default in config/wirekit.php:

'components' => [
    'range-slider' => ['show_values' => false],
],

Livewire Integration

Pass a single wire:model[.modifiers]="propName" directive on the component tag — the slider re-emits it on each hidden input as propName.min / propName.max so a Livewire array property gets live two-way binding. The slider keeps both thumbs in internal state seeded from min-value and max-value, so pass the bound range there as well:

public array $budget = ['min' => 1000, 'max' => 5000];
<x-wirekit::range-slider
    label="Budget"
    name="budget"
    :min="0"
    :max="10000"
    :step="100"
    :min-value="$budget['min']"
    :max-value="$budget['max']"
    wire:model.live="budget"
/>

Dragging either handle updates $budget['min'] / $budget['max'] on the server, and a range the server sets moves the thumbs. Without min-value and max-value the thumbs start at min and max rather than at the stored range, do not follow the server, and the first move sends a range counted from there, which replaces the stored one. A stored range outside the scale is held inside it: the thumbs stand at its ends, and the next move sends the range they show.

All wire:model modifiers (.live, .lazy, .debounce.500ms, .blur) flow through unchanged — the component re-emits them on both hidden inputs identically. Bound to an object whose min and max are declared int or float, such as a form object, each end is read as a number: wire:model.live goes out as wire:model.number.live, so the server's typed answer is not taken for a change that overwrites a newer position. An array property keeps the text it receives, as above.

For form submission without Livewire, the component still renders two hidden inputs ({name}[min] and {name}[max]) so a $_POST['budget'] (or Request::input('budget')) lands as a nested array.

An x-model on the tag is dropped, because the range reaches Alpine through neither handle: see An x-model on a WireKit component.

Width

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

<div class="max-w-md">
    <x-wirekit::range-slider label="Price" name="price" :min="0" :max="500" />
</div>

Mobile & Touch

On touch devices the thumb grows to a comfortable 28px (via @media (pointer: coarse)) and touch-action: none keeps a horizontal drag from scrolling the page. A tap on the track brings the nearer handle to that point, and the track takes the tap across a 24px band although it is drawn 8px tall. Discrete sliders (step > 1) with a readable step count render snap tick-marks.

Discrete slider with snap ticks
0 10
Range: 3 to 7

Form Submission

Two hidden inputs are rendered:

  • <input type="hidden" name="{name}[min]"> — lower handle value
  • <input type="hidden" name="{name}[max]"> — upper handle value

Reading the values server-side

The two thumbs submit as one array rather than two flat keys:

// 1. The request body a range-slider named "price" produces:
//    price%5Bmin%5D=10&price%5Bmax%5D=250

// 2. Read either half with dot notation — NOT `price_min`.
$request->input('price.min');   // "10"
$request->input('price.max');   // "250"

// 3. Or take the pair at once.
$request->input('price');       // ['min' => '10', 'max' => '250']

Validate it the same way:

// 1. Each half is its own rule path.
$request->validate([
    'price.min' => ['required', 'integer', 'min:0'],
    'price.max' => ['required', 'integer', 'gte:price.min'],
]);

This page documented {name}_min and {name}_max for a long time, and those keys never existed in the payload. $request->input('price_min') returns null — which a validator reads as a missing optional field rather than an error, so the wrong value reaches the database without anything failing. If you have _min / _max in a controller or a form request, that is the shape to change.

Named values

A range handle reads as its number. For a price that is exactly right; for a tier it is meaningless — "0 to 100" says nothing about Free or Enterprise. value-text-map gives each stop the word it deserves, and the word is what everyone gets: the handle badges show it, the bounds beneath the track show it, and a screen reader announces it.

A range slider at a fixed width
Free Enterprise
Range: Starter to Business

Stops the map does not name fall back to the number, so a partial map is safe — inventing a word for the gaps would be worse than showing the digit.

Optimistic UI

Pass the name of the Livewire method the slider should call and the range is sent the moment the gesture ends, then confirms or undoes itself when the server answers:

The method receives the pair, not the handle that moved:

<x-wirekit::range-slider
    name="budget"
    label="Budget"
    :min="0"
    :max="1000"
    :min-value="$from"
    :max-value="$to"
    optimistic="saveBudget"
/>
public function saveBudget(array $range): void
{
    [$this->from, $this->to] = $range;
}

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

@wirekitScripts
<script src="{{ asset('vendor/wirekit/wirekit-optimistic.js') }}" defer></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.

Optimistic range — accepted, refused, and a slow answer
0 1000
Range: 200 to 700
0 1000
Range: 200 to 700
0 1000
Range: 200 to 700

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.

When the value is sent is the part worth knowing. A drag produces a value on every frame, and sending each one would be a request storm; sending only the last would let the thumb run ahead of anything the server was ever told, so an undo would take back a value that was never on its way. So the send happens when the gesture is over — on release for a drag, and on each keypress for the keyboard, where one press is already a finished decision. Nothing is timed; the boundary is the event that ends the input.

The range is one value. If only one handle moved, an undo still restores both — the untouched one to what it already holds, which changes nothing. There is no per-handle bookkeeping to get out of step.

What a screen reader hears

The thumbs are tracked by the slider's own polite region while you drag, with or without the optimistic layer — that is not the layer talking.

At the commit, the layer announces once, hedged — "Saving" — so the new range is audible as provisional. Confirmation is silent: what was announced is what happened. Only a deviation speaks a second time, which is what makes an undo recognizable as an undo.

An aborted request announces nothing at all — nothing was refused.

Focus stays exactly where you put it. An undo arrives on the server's schedule, and moving focus then would take you out of your place for a reason you could not predict.

Error state
0 1000
Range: 200 to 800

The upper bound has to stay under your plan's limit.

Props

Prop Type Default Description
required bool false Marks the range as required. Emits aria-required="true" on the role="group" wrapper and renders the label indicator (*). The native attribute is not used: HTML ignores required on a range, which always has a value.
label string|null null Label text above the slider
error string|null null Validation message. Rendered below the control, announced politely, and wired with aria-invalid + aria-describedby
hint string|null null Help text below the slider
help string|null null An explanation of the field, shown in a tooltip from a question mark beside the label and read as the field's description. Needs a label to stand beside. See Help beside the label
name string|null null Base form field name. Submits array syntax — {name}[min] and {name}[max] — so the pair arrives as one array. See Reading the values server-side
id string|null auto-generated Base element id, on the group around the two thumbs. A <label for> cannot name a group: use label, aria-label or aria-labelledby
min int|float 0 Absolute minimum of the range
max int|float 100 Absolute maximum of the range
step int|float 1 Increment step for both handles, counted from min the way a native range input counts it. A decimal step writes values with the places the slider's numbers carry, so 0.1 steps give 0.3
minValue int|float|null min Initial position of the lower handle
maxValue int|float|null max Initial position of the upper handle. A start value outside min and max starts at the nearer end, two swapped values are put in order, and the handles start at least one step apart, the distance they keep while they move
optimistic string|null null Livewire method to call when the gesture ends, showing the new range before the server confirms it. Receives [min, max]. See Optimistic UI.
optimisticArgs array [] Extra arguments appended to the optimistic action call, after the new value — the row this control belongs to.
showValues bool|null null (falls back to config('wirekit.components.range-slider.show_values', true)) Toggle the floating value bubbles above each handle
disabled bool false Disabled state
valueTextMap array|null null Spoken value per stop, e.g. [0 => 'Free', 100 => 'Enterprise']. Each handle announces its own value.
scope string|null null Scoped personalization key
announceError bool|null null Render the error message in an aria-live="polite" region so a validation error that appears after render reaches a screen reader. Precedence: this prop, then the parent <x-wirekit::form :announce-errors>, then config('wirekit.a11y.announce_error'). Set false when the page runs its own live region.

Slots

Slot Description
labelAfter Markup after the label and its help question mark, in the same row and outside both, such as a badge that says a setting is not decided yet. See A Mark After the Label.

Accessibility

  • Each handle: role="slider" with aria-valuenow, aria-valuemin, aria-valuemax, and an aria-label that carries the group's own name — "Price range minimum" / "Price range maximum" when you pass label="Price range", and "Minimum" / "Maximum" when you pass no name at all
  • The group is wrapped with role="group", named by aria-labelledby pointing at the visible label (so the name on screen and the name announced are the same string)
  • Value changes are announced via aria-valuenow updates (native slider behavior)
  • Each handle's announced range is the range it can actually hold, not the whole track. The two handles never meet, so the lower one reports aria-valuemax one step below the upper handle's current value and the upper one reports aria-valuemin one step above the lower — both live, so they track as the handles move. On a 0–100 slider with step="10" the lower handle announces a maximum of 90, and pressing End lands it exactly there
  • A click or tap on the track brings the nearer handle to that point and focuses it, so a value can be set with a single pointer and no drag (WCAG 2.2 SC 2.5.7). The click that ends a drag does not count: releasing a handle over the track never moves the other one
  • Both handles have visible focus rings via focus-visible styling
  • An autofocus written on the tag goes to the lower handle, unless the slider is disabled
  • The filled track between handles is aria-hidden="true" — purely decorative
  • Disabled state sets native disabled on the hidden inputs (so the range is left out of a submitted form), aria-disabled="true" on the group and on both handles, and tabindex="-1" on the handles — they leave the tab order, the pointer and keyboard handlers are not rendered at all, and the whole group dims via --opacity-wk-disabled
  • A disabled <fieldset> around the slider locks it the same way while the page runs: both handles take aria-disabled="true" and tabindex="-1", ignore keys, presses and clicks on the track, and the slider dims. They come back when the fieldset is enabled again — see A Disabled Group

Keyboard Interaction

Each handle is independently focusable via Tab:

Key Action
Tab Move focus between the two handles
ArrowRight / ArrowUp Move handle up by step
ArrowLeft / ArrowDown Move handle down by step
PageUp Move handle up by step * 10
PageDown Move handle down by step * 10
Home Move handle to min (lower) or to one step above the lower handle (upper)
End Move handle to one step below the upper handle (lower) or to max (upper)

Handles cannot cross each other, and they cannot meet: the lower handle stops one step below the upper handle's value, and the upper handle stops one step above the lower one. Home and End land on the ends of the handle's own travel, which is the same bound the arrow keys clamp to.

Dragging a handle with a pointer also focuses it, and so does a click on the track, so either can be followed immediately by an arrow key to fine-tune the value.

Pitfalls

  • Don't use range-slider for two unrelated values. It's specifically for a min/max range over a shared scale. For two independent values pair two <x-wirekit::slider> instances.

Design Tokens

Token Purpose
--color-wk-accent Filled track and handle background
--color-wk-bg-elevated The ring around each handle
--color-wk-border-strong Unfilled track background, at 3:1 against the page, since the track shows where the values stand
--color-wk-ring Focus ring color on handles
--color-wk-text Value label text color
--color-wk-text-muted Hint text color
--radius-wk-sm Track border radius
--shadow-wk-sm Handle shadow
--transition-wk-duration Hover/focus transitions
--opacity-wk-disabled Dimmed state when disabled

Customization

Override defaults in config/wirekit.php:

'components' => [
    'range-slider' => ['min' => 0, 'max' => 100, 'step' => 1],
],

Further Reading

Updated in WireKit v2.66.0 (2026-10-13)

Was this page helpful?

Thank you for your feedback!

Voting requires cookies or local storage. What we store