Slider
Numeric range controls — a draggable track (single or two-thumb range, with discrete pips and scrubber variants), an angle dial, an exposure ticker, a media scrubber, or a springy elastic slider that stretches past its bounds.
Slider — A draggable, keyboard-accessible numeric control — single or two-thumb range, optional live value readout (top/bottom/left/right/tooltip), step ticks, and the discrete pips and large-thumb scrubber variants.
Preview
<div class="w-80 max-w-full space-y-6">
<x-ui.slider
name="volume"
label="{{ __('Volume') }}"
:value="40"
:step="10"
show-value
show-steps
suffix="%"
/>
<x-ui.slider
name="price"
label="{{ __('Price range') }}"
range
:value="20"
:value-end="80"
show-value
value-position="tooltip"
prefix="$"
/>
</div>
Installation
php artisan ui:add slider
Note
This component ships an Alpine behavior module at
resources/js/ui/slider.js. Import it once from your bundle so it registers on alpine:init:
import './slider.js';
Registry contract
php artisan ui:add slider
writes only the files below. The CLI validates each file hash before writing and asks before it replaces a local change, unless you pass --force.
-
resources/views/components/ui/slider.blade.php -
resources/js/ui/slider.js
- Registry dependencies
- None — installs on its own.
- Packages
-
composer: jml/brok:^0.2npm: alpinejs
Use with AI
A brief for your coding agent: install command, usage, props, guidance and the rules. Copy it, or open a prompt about this component in an assistant.
# Brok UI: Slider (`slider`)
A draggable, keyboard-accessible numeric control — single or two-thumb range, optional live value readout (top/bottom/left/right/tooltip), step ticks, and the discrete pips and large-thumb scrubber variants.
Brok UI is a Laravel Blade component registry. Installed components are plain Blade files the app owns.
## Install
```bash
php artisan ui:add slider
```
## Usage
```blade
<div class="w-80 max-w-full space-y-6">
<x-ui.slider
name="volume"
label="{{ __('Volume') }}"
:value="40"
:step="10"
show-value
show-steps
suffix="%"
/>
<x-ui.slider
name="price"
label="{{ __('Price range') }}"
range
:value="20"
:value-end="80"
show-value
value-position="tooltip"
prefix="$"
/>
</div>
```
## Props
- `name` (mixed|null, default `null`) — Hidden input name(s); range mode posts name[min]/name[max].
- `min` (int, default `0`) — Minimum value.
- `max` (int, default `100`) — Maximum value.
- `step` (int, default `1`) — Snap increment.
- `value` (int, default `0`) — Current value, or the low value in range mode.
- `valueEnd` (mixed|null, default `null`) — High value in range mode; defaults to max.
- `range` (bool, default `false`) — Renders two thumbs for a min/max range (track variant only).
- `showValue` (bool, default `false`) — Shows a live formatted readout.
- `valuePosition` (string, default `top`) — top, bottom, left, right or tooltip — where the readout renders.
- `showSteps` (bool, default `false`) — Shows tick marks over the track when the step count is legible (<=40).
- `variant` (track|pips|scrubber, default `track`) — track (thumb on a filled track), pips (discrete dot stops, no thumb) or scrubber (continuous fill with one large thumb).
- `label` (mixed|null, default `null`) — Accessible label for the thumb and readout heading.
- `prefix` (string, default ``) — Text prepended to the formatted readout.
- `suffix` (string, default ``) — Text appended to the formatted readout.
- `decimals` (int, default `0`) — Decimal places shown in the readout.
- `legendStart` (mixed|null, default `null`) — Optional word under the start end of the track.
- `legendEnd` (mixed|null, default `null`) — Optional word under the end end of the track.
## Use when
- Use when continuous adjustment or immediate visual feedback helps users choose a value.
- Collecting a single numeric value or a two-thumb range with a draggable, keyboard-accessible control.
- Needing the discrete pips look or the large-thumb scrubber look via variant, instead of building a separate component.
## Avoid when
- Do not use for precise values or broad ranges without good landmarks; direct entry is better when accuracy matters.
- Only use when approximate continuous adjustment is meaningful; pair it with direct entry when precision matters.
- Wanting a rotational dial; use angle-slider instead.
- Wanting a springy elastic drag feel; use elastic-slider instead.
- Wanting drag-anywhere-on-track scrubbing rather than a fixed thumb; use scrubber instead.
## Anti-patterns
- Using sliders for precise or unbounded values
## Rules
- Use the `<brok:slider>` tag (or `<x-ui.slider>`) in Blade; do not rewrite the component.
- Prefer the documented props and variants over utility-class overrides; when a utility must win, use the `!` important modifier.
- Use semantic design tokens (`bg-primary`, `text-muted-foreground`), never raw colour utilities.
- Keep the `data-slot` attributes; they are the styling and test hooks.
## Links
- Docs: https://brokui.dev/docs/components/slider
- Registry JSON (files, props, contract): https://brokui.dev/r/open/slider.json
Working in Claude Code, Cursor or Codex? Give the agent the whole registry through the MCP server or the Brok UI skill.
Examples
{{-- A labelled slider with a readout and legend words under the track — the
shape range-field used to wrap. --}}
<div class="mx-auto w-full max-w-sm">
<x-ui.field name="temperature" label="Temperature" hint="Lower is more precise; higher is more creative.">
<x-ui.slider name="temperature" :min="0" :max="2" :step="0.1" :value="0.7" :decimals="1" show-value legend-start="Precise" legend-end="Creative" />
</x-ui.field>
</div>
{{-- variant="pips": discrete dot stops with no thumb, for a settings panel.
variant="scrubber": the continuous fill with one large thumb. --}}
<div class="w-80 max-w-full space-y-6">
<x-ui.slider
variant="pips"
name="quality"
label="{{ __('Quality') }}"
:min="0"
:max="100"
:step="25"
:value="50"
suffix="%"
show-value
/>
<x-ui.slider
variant="scrubber"
name="volume"
label="{{ __('Volume') }}"
:min="0"
:max="11"
:step="1"
:value="7"
show-value
/>
</div>
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| name | mixed | null | null | Hidden input name(s); range mode posts name[min]/name[max]. |
| min | int | 0 | Minimum value. |
| max | int | 100 | Maximum value. |
| step | int | 1 | Snap increment. |
| value | int | 0 | Current value, or the low value in range mode. |
| valueEnd | mixed | null | null | High value in range mode; defaults to max. |
| range | bool | false | Renders two thumbs for a min/max range (track variant only). |
| showValue | bool | false | Shows a live formatted readout. |
| valuePosition | string | top | top, bottom, left, right or tooltip — where the readout renders. |
| showSteps | bool | false | Shows tick marks over the track when the step count is legible (<=40). |
| variant | track | pips | scrubber | track | track (thumb on a filled track), pips (discrete dot stops, no thumb) or scrubber (continuous fill with one large thumb). |
| label | mixed | null | null | Accessible label for the thumb and readout heading. |
| prefix | string | Text prepended to the formatted readout. | |
| suffix | string | Text appended to the formatted readout. | |
| decimals | int | 0 | Decimal places shown in the readout. |
| legendStart | mixed | null | null | Optional word under the start end of the track. |
| legendEnd | mixed | null | null | Optional word under the end end of the track. |
Slots
default— Not used by this component; present for attribute-bag compatibility.
Data slots
Stable hooks for CSS overrides and browser tests.
Behavior
- Dragging a thumb (or the pips control) or pressing arrow keys sets the value snapped to step; range=true renders two thumbs that clamp against each other; every change dispatches a bubbling slider-change event.
- showValue with valuePosition (top/bottom/left/right/tooltip) renders a live formatted readout; showSteps adds tick marks over the track when the step count is legible.
- Installs a JavaScript behavior module when the registry item includes resources/js/ui files.
- Declares registry capability flags: a11y, interactive, responsive, rtl, darkMode, localized.
Guidance
Adjust an intuitive continuous value.
Use when
- Use when continuous adjustment or immediate visual feedback helps users choose a value.
- Collecting a single numeric value or a two-thumb range with a draggable, keyboard-accessible control.
- Needing the discrete pips look or the large-thumb scrubber look via variant, instead of building a separate component.
Avoid when
- Do not use for precise values or broad ranges without good landmarks; direct entry is better when accuracy matters.
- Only use when approximate continuous adjustment is meaningful; pair it with direct entry when precision matters.
- Wanting a rotational dial; use angle-slider instead.
- Wanting a springy elastic drag feel; use elastic-slider instead.
- Wanting drag-anywhere-on-track scrubbing rather than a fixed thumb; use scrubber instead.
Use instead
- Number field for precise entry
- Select for discrete values
Anti-patterns
- Using sliders for precise or unbounded values
- Anatomy
- Theming hooks
Accessibility
- Keyboard
- ArrowUp ArrowDown ArrowLeft ArrowRight Home End
- Focus
managed
- Each thumb (or the pips control) is a role="slider" with aria-valuemin/max/now, positioned with logical inset-inline-start so it mirrors under dir="rtl".
- Semantic HTML and a stable
data-slotattribute for styling and scripting hooks. - Focus-visible rings use the
ringtoken, so keyboard focus is always visible. - Disabled and invalid states are conveyed to assistive tech, not by color alone.
- Targets WCAG 2.2 AA; verify contrast in light, dark, admin and customer surfaces in the preview.
- Labels go through
__()and layout uses logical properties (ms-*,text-start), so it mirrors underdir="rtl"— flip the preview to RTL to confirm. - Dark mode uses the same semantic tokens under the
darkclass; high contrast follows forced-color system tokens.
Livewire
Add a stable wire:key when Livewire can reorder this interactive component.
<div wire:key="slider-{{ $record->id }}">
<div class="w-80 max-w-full space-y-6">
<x-ui.slider
name="volume"
label="{{ __('Volume') }}"
:value="40"
:step="10"
show-value
show-steps
suffix="%"
/>
<x-ui.slider
name="price"
label="{{ __('Price range') }}"
range
:value="20"
:value-end="80"
show-value
value-position="tooltip"
prefix="$"
/>
</div>
</div>
Validation
Validation support: native. Keep the error message connected with aria-describedby.
<form wire:submit="save" class="space-y-2">
<brok:slider
wire:model="value"
:aria-invalid="$errors->has('value') ? 'true' : 'false'"
aria-describedby="value-error"
/>
@error('value')
<p id="value-error" role="alert">{{ $message }}</p>
@enderror
<brok:button type="submit" wire:loading.attr="disabled">
<span wire:loading.remove>Save</span>
<span wire:loading>Saving…</span>
</brok:button>
</form>
Source
The exact, editable files ui:add writes
into your app. Previews render this same code; there are no preview-only components.
@props([
'name' => null,
'min' => 0,
'max' => 100,
'step' => 1,
'value' => 0,
'valueEnd' => null,
'range' => false,
'showValue' => false,
'valuePosition' => 'top',
'showSteps' => false,
// track (default): a thumb on a filled track. pips: discrete dot stops on
// a thin rail, no thumb — the settings-panel selector (single value).
// scrubber: the track with one large thumb (single value).
'variant' => 'track',
'label' => null,
'prefix' => '',
'suffix' => '',
'decimals' => 0,
// Optional words under the two ends of the track ("Precise" … "Creative").
'legendStart' => null,
'legendEnd' => null,
])
@php
$styles = require base_path(config('ui.component_path', 'resources/views/components/ui').'/_styles.php');
$variant = $styles['normalizeVariant']($variant);
$variant = in_array($variant, ['track', 'pips', 'scrubber'], true) ? $variant : 'track';
$recipe = $styles['slider'];
$min = (float) $min;
$max = (float) $max;
$step = (float) $step;
// pips and scrubber are single-value looks.
$range = $variant === 'track' && filter_var($range, FILTER_VALIDATE_BOOL);
$showValue = filter_var($showValue, FILTER_VALIDATE_BOOL);
$showSteps = filter_var($showSteps, FILTER_VALIDATE_BOOL);
$decimals = max(0, (int) $decimals);
$value = max($min, min($max, (float) $value));
$valueEnd = $valueEnd === null ? $max : max($min, min($max, (float) $valueEnd));
$positions = ['top', 'bottom', 'left', 'right', 'tooltip'];
$pos = in_array($valuePosition, $positions, true) ? $valuePosition : 'top';
$hasReadout = $showValue && in_array($pos, ['top', 'bottom', 'left', 'right'], true);
$isTooltip = $showValue && $pos === 'tooltip';
$inline = $hasReadout && in_array($pos, ['left', 'right'], true);
$readoutFirst = in_array($pos, ['top', 'left'], true);
// Step pips are only legible for a modest number of stops; skip otherwise.
$stepCount = $step > 0 ? (int) round(($max - $min) / $step) : 0;
$pips = ($showSteps && $stepCount > 0 && $stepCount <= 40) ? $stepCount : 0;
// The discrete variant needs its stops; clamp to something legible.
$stops = $variant === 'pips' ? max(1, min(40, $stepCount ?: 1)) : 0;
$thumbClass = $recipe['thumb'].' '.$recipe['thumbCentered'].' '.$recipe['thumbSizes'][$variant === 'scrubber' ? 'lg' : 'md'];
// slider-pips renders this component under its own name; the browser
// keeps only the first of two identical attributes, so read the override
// out of the bag and print exactly one.
$slotName = (string) ($attributes->get('data-slot') ?: 'slider');
// No-JS fallback formatting for the readout / tooltip text.
$fmt = fn ($v) => $prefix.($decimals > 0 ? number_format((float) $v, $decimals) : rtrim(rtrim(number_format((float) $v, 4, '.', ''), '0'), '.')).$suffix;
$fallback = $range ? $fmt($value).' – '.$fmt($valueEnd) : $fmt($value);
$config = [
'min' => $min, 'max' => $max, 'step' => $step,
'value' => $value, 'valueEnd' => $valueEnd, 'range' => $range,
'decimals' => $decimals, 'prefix' => (string) $prefix, 'suffix' => (string) $suffix,
];
@endphp
{{--
Slider — a draggable, keyboard-accessible numeric control. Supports a single
thumb (default) or a two-thumb `range`, an optional live value readout
(`showValue` + `valuePosition`: top / bottom / left / right / tooltip), and
optional step ticks over the track. `variant="pips"` is the discrete
settings-panel look (dot stops, no thumb) and `variant="scrubber"` the
continuous fill with one large thumb; both are single-value. The behaviour layer (`uiSlider`) keeps every value snapped to
`step`, clamps thumbs against each other in range mode, and positions the fill /
thumbs with the logical `inset-inline-start` so it mirrors under dir="rtl". Every
value change dispatches a bubbling `slider-change` event for wrappers/consumers.
--}}
<div
data-slot="{{ $slotName }}"
data-variant="{{ $variant }}"
@if ($range) data-range="true" @endif
x-data="uiSlider({{ \Illuminate\Support\Js::from($config) }})"
{{-- Modelable: `x-model` (Alpine) and `wire:model` (Livewire binds through
the same x-model contract) attach to the root. Inert without either. --}}
x-modelable="model"
{{ $attributes->except('data-slot')->merge(['class' => 'w-full '.($inline ? 'flex items-center gap-3' : 'flex flex-col gap-2')]) }}
>
@if ($hasReadout && $readoutFirst)
@if ($inline)
<div class="shrink-0 text-sm">
@if ($label) <span class="block font-medium text-foreground">{{ __($label) }}</span> @endif
<output class="{{ $recipe['readout'] }}" x-text="readout()">{{ $fallback }}</output>
</div>
@else
<div class="flex items-baseline justify-between gap-2 text-sm">
<span class="font-medium text-foreground">{{ $label ? __($label) : '' }}</span>
<output class="{{ $recipe['readout'] }}" x-text="readout()">{{ $fallback }}</output>
</div>
@endif
@endif
@if ($variant === 'pips')
{{-- Discrete stops: the control itself is the slider (no thumb); each
dot is a click target and the fill runs up to the current stop. --}}
<div
data-slot="slider-control"
x-ref="track"
role="slider"
tabindex="0"
aria-orientation="horizontal"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="value"
aria-label="{{ __($label ?? 'Value') }}"
@keydown="onKey($event)"
@pointerdown="startDrag($event)"
class="{{ $recipe['control'] }} {{ $inline ? 'flex-1' : 'w-full' }} justify-between py-2 focus-visible:outline-none focus-visible:ring-[length:var(--ring-width)] focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background"
>
<div aria-hidden="true" data-slot="slider-track" class="{{ $recipe['rail'] }}"></div>
<div aria-hidden="true" data-slot="slider-range" class="{{ $recipe['railFill'] }}" :style="`width:${percent(value)}%`"></div>
@for ($i = 0; $i <= $stops; $i++)
@php $stop = $min + $i * (($max - $min) / $stops); @endphp
<span
aria-hidden="true"
data-slot="slider-pip"
@click="setActiveValue({{ $stop }})"
class="{{ $recipe['pip'] }}"
:class="value >= {{ $stop }} ? '{{ $recipe['pipOn'] }}' : '{{ $recipe['pipOff'] }}'"
></span>
@endfor
</div>
@else
<div
data-slot="slider-control"
@pointerdown="startDrag($event)"
class="{{ $recipe['control'] }} {{ $inline ? 'flex-1' : 'w-full' }} py-2"
>
<div
x-ref="track"
data-slot="slider-track"
class="{{ $recipe['track'] }}"
>
<div data-slot="slider-range" class="{{ $recipe['fill'] }}" :style="`inset-inline-start:${fillStart()}%; width:${fillWidth()}%`"></div>
</div>
@if ($pips)
@for ($i = 0; $i <= $pips; $i++)
<span
aria-hidden="true"
data-slot="slider-tick"
class="{{ $recipe['tick'] }}"
style="inset-inline-start: {{ rtrim(rtrim(number_format($i / $pips * 100, 4, '.', ''), '0'), '.') }}%"
></span>
@endfor
@endif
{{-- Low / single thumb. Keeps the bare onKey($event) hook the a11y guard asserts. --}}
<span
data-slot="slider-thumb"
role="slider"
tabindex="0"
aria-orientation="horizontal"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="value"
aria-label="{{ $range ? __('Minimum') : __($label ?? 'Value') }}"
@keydown="onKey($event)"
@pointerdown.stop="startDrag($event, 'low')"
class="group/thumb {{ $thumbClass }}"
:style="`inset-inline-start:${percent(value)}%`"
>
@if ($isTooltip)
<span
aria-hidden="true"
class="{{ $recipe['tooltip'] }}"
x-text="display(value)"
>{{ $fmt($value) }}</span>
@endif
</span>
@if ($range)
<span
data-slot="slider-thumb"
role="slider"
tabindex="0"
aria-orientation="horizontal"
:aria-valuemin="min"
:aria-valuemax="max"
:aria-valuenow="valueEnd"
aria-label="{{ __('Maximum') }}"
@keydown="onKey($event, 'high')"
@pointerdown.stop="startDrag($event, 'high')"
class="group/thumb {{ $thumbClass }}"
:style="`inset-inline-start:${percent(valueEnd)}%`"
>
@if ($isTooltip)
<span
aria-hidden="true"
class="{{ $recipe['tooltip'] }}"
x-text="display(valueEnd)"
>{{ $fmt($valueEnd) }}</span>
@endif
</span>
@endif
</div>
@endif
@if ($hasReadout && ! $readoutFirst)
@if ($inline)
<div class="shrink-0 text-sm">
@if ($label) <span class="block font-medium text-foreground">{{ __($label) }}</span> @endif
<output class="{{ $recipe['readout'] }}" x-text="readout()">{{ $fallback }}</output>
</div>
@else
<div class="flex items-baseline justify-between gap-2 text-sm">
<span class="font-medium text-foreground">{{ $label ? __($label) : '' }}</span>
<output class="{{ $recipe['readout'] }}" x-text="readout()">{{ $fallback }}</output>
</div>
@endif
@endif
@if ($legendStart !== null || $legendEnd !== null)
<div data-slot="slider-legend" class="flex justify-between text-xs text-muted-foreground" aria-hidden="true">
<span>{{ __($legendStart ?? '') }}</span>
<span>{{ __($legendEnd ?? '') }}</span>
</div>
@endif
@if ($name)
@if ($range)
<input type="hidden" name="{{ $name }}[min]" :value="value" />
<input type="hidden" name="{{ $name }}[max]" :value="valueEnd" />
@else
<input type="hidden" name="{{ $name }}" :value="value" />
@endif
@endif
</div>
/**
* Slider behavior (spec §19 behavior layer).
*
* Holds one numeric `value` — or, in `range` mode, a `value`/`valueEnd` pair —
* each clamped to [min, max] and snapped to `step`. Thumbs and the range fill are
* positioned with the logical `inset-inline-start`, so the component mirrors
* automatically under `dir="rtl"`; pointer and keyboard input account for direction
* so dragging and arrow keys always feel correct. In range mode the two thumbs clamp
* against each other so the low value can never cross the high value.
*
* Whenever a value changes (drag, keyboard, or programmatic set) the root element
* dispatches a bubbling `slider-change` CustomEvent. For a single slider the detail is
* `{ value }`; in range mode it is `{ value, valueEnd }`, so wrappers (e.g.
* range-field) and consumers can react without reaching into Alpine internals.
*
* Self-registers on `alpine:init` so import order does not matter — import this
* file once from your bundle (e.g. resources/js/ui/index.js).
*/
document.addEventListener('alpine:init', () => {
window.Alpine.data('uiSlider', (config = {}) => ({
min: config.min ?? 0,
max: config.max ?? 100,
step: config.step ?? 1,
range: config.range ?? false,
value: config.value ?? 0,
valueEnd: config.valueEnd ?? config.max ?? 100,
decimals: config.decimals ?? 0,
prefix: config.prefix ?? '',
suffix: config.suffix ?? '',
active: 'low',
// Bound listener refs so we can remove exactly what we added.
_move: null,
_up: null,
init() {
this.value = this.clamp(this.value);
this.valueEnd = this.clamp(this.valueEnd);
if (this.range) this.enforceOrder();
// Emit a bubbling event on every value change so wrappers (e.g.
// range-field) and consumers can mirror the value live.
this.$watch('value', () => this.emit());
this.$watch('valueEnd', () => this.emit());
},
/**
* Modelable value (`x-modelable="model"` on the root): the number, or a
* `[low, high]` pair for a range slider. `x-model` and Livewire's
* `wire:model` read and write it through the same contract.
*/
get model() {
return this.range ? [this.value, this.valueEnd] : this.value;
},
set model(v) {
if (this.range) {
const pair = Array.isArray(v) ? v : (v && typeof v === 'object' ? [v.min ?? v.value, v.max ?? v.valueEnd] : [v, v]);
this.value = this.clamp(Number(pair[0] ?? this.min));
this.valueEnd = this.clamp(Number(pair[1] ?? this.max));
this.enforceOrder();
return;
}
this.value = this.clamp(Number(v ?? this.min));
},
destroy() {
// Remove any listeners still attached if unmounted mid-drag.
window.removeEventListener('pointermove', this._move);
window.removeEventListener('pointerup', this._up);
},
emit() {
this.$el.dispatchEvent(
new CustomEvent('slider-change', {
detail: this.range
? { value: this.value, valueEnd: this.valueEnd }
: { value: this.value },
bubbles: true,
}),
);
},
clamp(v) {
v = Math.round(v / this.step) * this.step;
v = Math.min(this.max, Math.max(this.min, v));
// Round away binary-float dust (e.g. 0.30000000000000004) at the readout.
const fromStep = String(this.step).split('.')[1]?.length ?? 0;
const d = Math.max(this.decimals, fromStep);
return d ? Number(v.toFixed(d)) : v;
},
percent(v) {
return this.max > this.min
? ((v - this.min) / (this.max - this.min)) * 100
: 0;
},
fillStart() {
return this.range ? this.percent(this.value) : 0;
},
fillWidth() {
return this.range
? this.percent(this.valueEnd) - this.percent(this.value)
: this.percent(this.value);
},
// Keep the low thumb at or below the high thumb by pushing whichever thumb
// is being moved up against the other one.
enforceOrder() {
if (this.value > this.valueEnd) {
if (this.active === 'low') this.value = this.valueEnd;
else this.valueEnd = this.value;
}
},
setActiveValue(v) {
v = this.clamp(v);
if (this.active === 'high') this.valueEnd = v;
else this.value = v;
if (this.range) this.enforceOrder();
},
nearest(v) {
if (!this.range) return 'low';
return Math.abs(v - this.value) <= Math.abs(v - this.valueEnd) ? 'low' : 'high';
},
valueFromClientX(clientX) {
const r = this.$refs.track.getBoundingClientRect();
let ratio = (clientX - r.left) / r.width;
if (getComputedStyle(this.$el).direction === 'rtl') {
ratio = 1 - ratio;
}
return this.min + ratio * (this.max - this.min);
},
startDrag(e, which) {
const v = this.valueFromClientX(e.clientX);
this.active = which ?? this.nearest(v);
this.setActiveValue(v);
// Bind once so the same refs can be removed later (here or in destroy()).
this._move = (ev) => this.setActiveValue(this.valueFromClientX(ev.clientX));
this._up = () => {
window.removeEventListener('pointermove', this._move);
window.removeEventListener('pointerup', this._up);
};
window.addEventListener('pointermove', this._move);
window.addEventListener('pointerup', this._up);
},
onKey(e, which) {
this.active = which ?? 'low';
const cur = this.active === 'high' ? this.valueEnd : this.value;
const rtl = getComputedStyle(this.$el).direction === 'rtl';
const dec = () => this.setActiveValue(cur - this.step);
const inc = () => this.setActiveValue(cur + this.step);
switch (e.key) {
case 'ArrowLeft':
case 'ArrowDown':
e.preventDefault();
rtl && e.key === 'ArrowLeft' ? inc() : dec();
break;
case 'ArrowRight':
case 'ArrowUp':
e.preventDefault();
rtl && e.key === 'ArrowRight' ? dec() : inc();
break;
case 'Home':
e.preventDefault();
this.setActiveValue(this.min);
break;
case 'End':
e.preventDefault();
this.setActiveValue(this.max);
break;
}
},
display(v) {
const n = this.decimals > 0 ? Number(v).toFixed(this.decimals) : String(v);
return `${this.prefix}${n}${this.suffix}`;
},
readout() {
return this.range
? `${this.display(this.value)} – ${this.display(this.valueEnd)}`
: this.display(this.value);
},
}));
});
Ownership & lifecycle
Owner, release state, review evidence and adoption for this item.
- Owner
- Platform UI (@JoshJML)
- Current version
-
1.5.1 - Status
- Stable
- License
-
open - Accessibility reviewed
- No review date recorded
- Last breaking change
- No date recorded
- Deprecation
- Not deprecated
- Contract
-
v6 - Foundation
-
≥ 1.0.0