Slider
A hand-drawn range slider for selecting a numeric value.
Overview
A Slider selects a numeric value from a continuous or stepped range by dragging a thumb along a
track. Use Input type="number" instead when a precise typed value matters more than a quick
visual adjustment.
Anatomy
A sketchy rail (a thin rectangle) with a filled portion (from the start to the thumb) and a round
thumb — all three shapes computed directly from @ghds/sketch-core, sharing one measured box and
seed. On React and Web Components, a real, invisible <input type="range"> spans the full track
and carries every interaction (dragging, clicking the track, and native Arrow/Home/End/PageUp/
PageDown keyboard behavior) with zero custom pointer/keydown code. React Native has no such native
primitive, so dragging is hand-rolled with PanResponder (built into React Native core — no new
gesture-library dependency).
Variants & States
| State | Status | Notes |
|---|---|---|
| Default | Implemented | — |
| Hover | Implemented | The filled portion and thumb darken slightly (CSS/native pseudo-states — no JS-tracked hover flag needed). |
| Focus | Implemented | A focus ring appears around the thumb. |
| Disabled | Implemented | — |
| Two-thumb range | Not implemented | Not in v1 — "range" in the ticket names a possible future dual-thumb mode, not a v1 requirement. |
| Vertical orientation | Not implemented | — |
| Value tooltip while dragging | Not implemented | A consumer wanting a live readout derives it from a controlled value. |
Usage
Do
- Use Slider for a value with a clear, visible min/max range (e.g. volume, rating, brightness).
- Label the value being set ("Volume"), not the current reading.
Don't
- Don't use Slider when the exact value must be typed precisely — use Input type="number" instead.
- Don't expect a two-thumb range mode in v1 — a single thumb only.
Accessibility
- React / Web Components: the real, invisible
<input type="range">gives an implicitrole="slider"plus browser-computedaria-valuenow/aria-valuemin/aria-valuemaxfor free — no custom ARIA wiring needed.aria-valuetextpasses through as a plain attribute for a custom spoken value. - React Native:
accessibilityRole="adjustable"plusaccessibilityValue={{min, max, now}}andaccessibilityActions/onAccessibilityAction(increment/decrement) are the standard RN pattern for VoiceOver/TalkBack. NeitheraccessibilityValuenoraccessibilityState.disabledhas a React Native Web equivalent mapping — confirmed directly againstcreateDOMProps— soaria-valuemin/aria-valuemax/aria-valuenow/aria-disabledare set directly for the web target, the same gap class already documented for Select’saria-expandedand Switch’saria-checked.onAccessibilityActionitself has no React Native Web wiring at all (confirmed directly) — it is a native-only (iOS/Android) affordance, verified manually perpackages/react-native/AGENTS.md’s device/simulator requirement rather than by automated test.focusable/tabIndexmake the control a real keyboard tab-stop on the web target (role="slider"isn’t one of React Native Web’s auto-focusable roles), but arrow-key step adjustment while focused is not yet wired there in v1 —onKeyDownisn’t part of real React Native’sViewprops, so it isn’t a lightweight addition; dragging with a mouse/touch works on every platform. - See the Accessibility Guide for general conventions.
Content
The label names the value being adjusted (“Volume”, “Rating”), not the current reading or an instruction.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Visible label, associated via Radix Label. |
value | number | — | Controlled value. |
defaultValue | number | — | Initial value when uncontrolled. |
min | number | 0 | Minimum value. |
max | number | 100 | Maximum value. |
step | number | 1 | Increment step. |
disabled | boolean | false | Disables interaction. |
...rest | InputHTMLAttributes | — | All other native input attributes pass through (e.g. onChange, aria-valuetext, name). |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | 0 | Reflected attribute. Mirrored to the form value. |
min | number | 0 | Minimum value. |
max | number | 100 | Maximum value. |
step | number | 1 | Increment step. |
disabled | boolean | false | Disables interaction and form participation. |
name | string | — | Submitted control name. |
label | string | — | Optional visible label, also the accessible name. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Visible, accessible label — required. |
value | number | — | Controlled value. |
defaultValue | number | — | Initial value when uncontrolled. |
min | number | 0 | Minimum value. |
max | number | 100 | Maximum value. |
step | number | 1 | Increment step. |
onValueChange | (value: number) => void | — | Fires with the next value while dragging. |
disabled | boolean | false | Disables interaction. |
testID | string | — | Test handle for queries. |
Code examples
// React — @ghds/react (real <input type="range">; read event.target.value)
import { Slider } from '@ghds/react/slider';
import { useState } from 'react';
const [volume, setVolume] = useState(30);
<Slider label="Volume" value={volume} onChange={(event) => setVolume(Number(event.target.value))} />;
<Slider label="Rating" min={0} max={10} step={1} defaultValue={7} />;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/slider';
</script>
<gh-slider label="Volume" value="30"></gh-slider>
<gh-slider label="Rating" min="0" max="10" step="1" value="7"></gh-slider>
// React Native — @ghds/react-native
// Drag only — no arrow-key stepping on React Native. Value arrives via onValueChange.
import { Slider } from '@ghds/react-native/slider';
<Slider label="Volume" value={volume} onValueChange={setVolume} />;
Live Demo
React and Web Components render live below. React Native can’t run in a browser without additional
react-native-web wiring (not set up in this site), so its usage is shown as a code sample instead.