Range
A slider for a value picked by feel along a continuous scale, built on the native range input.
When to Use
type="number", or Stepper for fixed jumps the reader counts. For a handful of named choices use Select or Radio Card Group.
Variants
Default
The browser's own slider, tinted with --brand through accent-color. No script is needed for it to work. Pass valueText so a screen reader hears what the value means, and update aria-valuetext as it moves.
15 minutes
<?= ws_range('break_length', [
'label' => 'Break length',
'min' => 5,
'max' => 30,
'step' => 5,
'value' => 15,
'valueText' => '15 minutes',
]) ?>Bare
The track and thumb go transparent and the page draws the handle underneath. The input stays real: focusable, labelled (here with hideLabel), and moved with the arrow keys. The focus ring goes round the thumb, where the drawn handle is. The input is one thumb tall (--ws-range-thumb, 44px by default), so position its wrapper centred on the handle and spanning the travel plus half a thumb each side.
<?= ws_range('split', [
'label' => 'Move the divider between before and after',
'hideLabel' => true,
'min' => 20,
'max' => 80,
'value' => 50,
'valueText' => 'Before 50%, After 50%',
'variant' => 'bare',
]) ?>States
Set by the session you joined
| State | Behavior |
|---|---|
| Default | The browser paints the track and thumb; accent-color fills the travelled part with the brand colour. |
| Hover | Pointer cursor on the default variant, ew-resize on the bare one. The bare variant's drawn handle can react with :has(.ws-range__input:hover). |
| Focus | The platform focus ring (--focus-ring) round the input on the default variant, and round the thumb on the bare one. |
| Disabled | Native disabled plus .ws-range--disabled, which dims the whole field. Skipped by Tab. |
<?= ws_range('room_size', ['label' => 'Room size', 'value' => 12, 'disabled' => true]) ?>Real-World Usage
The homepage candidate at /index4.php uses the bare variant for its hinge: the page draws a divider and a grip between how a workshop lives now and how it lives in Workshopr, and the range sits over the grip so a reader can drag it or move it with the arrow keys. The page's script listens for input, moves the split and rewrites aria-valuetext:
const range = document.querySelector('#i4-split');
range.addEventListener('input', () => {
const now = Number(range.value);
figure.style.setProperty('--i4-split', now);
range.setAttribute('aria-valuetext', `How it lives now ${now}%, In Workshopr ${100 - now}%`);
});Options
| Option | Type | Default | Purpose |
|---|---|---|---|
$name | string | '' | Positional. The input's name. |
label | string | null | A real <label for>. Always pass one: a range with no name is announced only as "slider". |
hideLabel | bool | false | Hides the label visually (.ws-visually-hidden) while it still names the input. For when the page shows the meaning another way. |
description | string | null | Sub-text under the slider, wired as aria-describedby. |
min / max | int|float | 0 / 100 | The ends of the scale. |
step | int|float | 1 | Distance between values, and how far one arrow key moves it. |
value | int|float | halfway | The starting value. |
valueText | string | null | What the starting value means, as aria-valuetext ("15 minutes"). Update it from script as the value moves. |
variant | string | 'default' | default or bare. |
disabled | bool | false | Native disabled state plus the dimmed wrapper class. |
id | string | 'ws-range-' . uniqid() | Input ID. Set it when script needs a stable handle. |
class | string | '' | Additional classes on the wrapper, which is what a page positions. |
attrs | array | [] | Extra attributes on the input, for data-* hooks. |
Accessibility
| Concern | Behavior |
|---|---|
| Semantics | A native <input type="range">, so the slider role, value, min and max come from the browser. Both variants keep it; the bare one only makes its paint transparent. |
| Keyboard | Native: Tab to reach, arrow keys move one step, Page Up/Page Down move further, Home/End jump to the ends. |
| Labelling | A <label for>, visually hidden with hideLabel and never removed. The description is announced after it through aria-describedby. |
| Value | Without aria-valuetext a screen reader reads the bare number. Pass valueText and keep it current from script so it hears "15 minutes" or "Before 40%, After 60%". |
| Target size | The default variant's box is 44px tall. The bare variant's thumb is 44px square unless the page sets --ws-range-thumb; keep it at 44px or more. |
| Forced colours | The bare variant's input keeps a transparent outline rather than none, so Windows High Contrast repaints a visible ring. |
Tokens
| Token | Used for |
|---|---|
--brand | accent-color on the default variant |
--focus-ring / --focus-ring-offset | Focus ring round the default variant |
--focus-ring-width / --focus-ring-color / --focus-ring-halo | Focus ring and halo round the bare variant's thumb |
--text-small / --text-muted | Description |
--space-2 | Gap between label, slider and description |
--radius-full | Bare thumb shape |
CSS Classes
| Class | Purpose |
|---|---|
.ws-range | Field wrapper: label, slider, description |
.ws-range--bare | Transparent track and thumb for a page-drawn handle; sets --ws-range-thumb |
.ws-range--disabled | Dimmed disabled field |
.ws-range__label | The label (also carries .ws-field__label) |
.ws-range__input | The range input |
.ws-range__description | Sub-text |
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_range() helper function |
includes/components/range.php | Template: label, input and description wiring |
includes/components/components.css | Styles (.ws-range rules) |