Slider
Lets people choose a value, or a range of values, by dragging a handle along a track, continuously or in steps.
- Category
- Selection
- Tokens
comp.slider- Level
- Base
- Used in
- Slider field
- Web
tanrei-sliderschedule - SwiftUI
TanreiSliderschedule - Jetpack Compose
TanreiSliderschedule - Flutter
TanreiSliderschedule - React Native
Sliderschedule
Generated from docs/status/status.json, specs/slider.spec.json
Continuous, stepped with stops, and a range
Playground
A slider chooses one value between min and max (0 and 100 by default). With range it has two handles and chooses the span from startValue to value. step 0 is continuous; above 0 the values snap to min plus whole multiples of step.
The slider draws no label. label names it for assistive technology, and a visible label is your own text beside the slider, in the same words. A range needs startLabel for its start handle too.
Two events report a change. input fires for every change while the person drags, and change fires once when the change is finished: the press ended, or a key or an assistive-technology adjustment was made. A value you set yourself fires neither. Each platform names them in its own way: onValueChange and onValueChangeFinished on Compose, onChanged and onChangeEnd on Flutter, onValueChange and onSlidingComplete on React Native, and on SwiftUI the binding and onCommit.
The revision that gave the slider its round thumb, and the review that made its ring heavier, changed only how it looks, so every sample below is current. The web draws the current look. SwiftUI, Compose, Flutter and React Native are lagging: they still draw the previous look, a solid round handle with the tracks cut at its edge and one stop-dot size for every size, until they catch up.
<!-- The slider draws no label: put your visible label beside it, in the same words. -->
<tanrei-slider name="volume" label="Volume" value="40" value-indicator="active"></tanrei-slider>
<!-- Stepped, with a dot at every step. -->
<tanrei-slider label="Brightness" value="30" step="10" stops></tanrei-slider>
<!-- A range submits start-value, then value, under the same name. -->
<tanrei-slider name="price" range min="0" max="500" step="10" start-value="100" value="300"
label="Maximum price" start-label="Minimum price"></tanrei-slider>
<script type="module">
import '@tanrei/web/components/slider';
const volume = document.querySelector('[name="volume"]');
// The element sets its own value before it fires input and change.
volume.addEventListener('input', () => preview(volume.value));
volume.addEventListener('change', () => save(volume.value));
</script>import { Slider } from '@tanrei/react';
<Slider
label="Volume"
value={volume}
valueIndicator="active"
onInput={(e) => setVolume(e.currentTarget.value)}
onChange={(e) => save(e.detail.value)}
/>
<Slider
range
min={0}
max={500}
step={10}
startValue={minPrice}
value={maxPrice}
label="Maximum price"
startLabel="Minimum price"
onInput={(e) => setPrices(e.currentTarget.startValue, e.currentTarget.value)}
/>import Tanrei
// Every change writes the binding; onCommit runs when a change is finished.
TanreiSlider("Volume", value: $volume, in: 0...100, onCommit: save)
TanreiSlider("Brightness", value: $brightness, in: 0...100, step: 10)
// A range is the initializer whose binding is a ClosedRange: lowerBound is the start handle.
TanreiSlider("Maximum price", startLabel: "Minimum price", value: $prices, in: 0...500, step: 10)import io.github.chnotchy.tanrei.components.TanreiSlider
// State hoisting: move value from onValueChange.
TanreiSlider(
value = volume,
onValueChange = { volume = it },
contentDescription = "Volume",
onValueChangeFinished = ::save, // it carries the value, unlike foundation's
)
// step is a size, not Material's count of steps.
TanreiSlider(value = brightness, onValueChange = { brightness = it }, step = 10f, contentDescription = "Brightness")
// A range is the overload whose value is a ClosedFloatingPointRange.
TanreiSlider(
value = prices,
onValueChange = { prices = it },
valueRange = 0f..500f,
step = 10f,
contentDescription = "Maximum price",
startContentDescription = "Minimum price",
)import 'package:tanrei/tanrei.dart';
TanreiSlider(
value: volume,
onChanged: (double value) => setState(() => volume = value), // null disables it
onChangeEnd: save,
semanticLabel: 'Volume',
);
// step is a size, not Material's divisions.
TanreiSlider(value: brightness, step: 10, onChanged: (double value) => setState(() => brightness = value), semanticLabel: 'Brightness');
TanreiSlider.range(
start: minPrice,
end: maxPrice,
max: 500,
step: 10,
onChanged: (double start, double end) => setState(() {
minPrice = start;
maxPrice = end;
}),
semanticLabel: 'Maximum price',
startSemanticLabel: 'Minimum price',
);import { Slider } from '@tanrei/react-native';
<Slider value={volume} onValueChange={setVolume} onSlidingComplete={save} accessibilityLabel="Volume" />
<Slider value={brightness} step={10} stops onValueChange={setBrightness} accessibilityLabel="Brightness" />
// A range: both callbacks receive (value, startValue).
<Slider
range
minimumValue={0}
maximumValue={500}
step={10}
startValue={minPrice}
value={maxPrice}
onValueChange={(value, startValue) => setPrices(startValue, value)}
accessibilityLabel="Maximum price"
startAccessibilityLabel="Minimum price"
/>Sizes, centered, value indicator, vertical and disabled
In the default system the thumb is a ring: a 20 circle filled with the surface color, inside an outline in the active track’s color. The outline is 3 wide and is drawn inside the circle’s edge, so it does not change the thumb’s size: it leaves a 14 disc of the surface color, and a wider outline would leave a smaller one. The track and the active track run under the thumb and meet at its center, so the active track reads as one line that ends in the ring. The thumb is 20 at every size, on tracks of 4, 6 and 8.
The stop dots, and the dot that marks the end of the travel, are half as thick as the track: 2, 3 and 4 at small, medium and large. The value indicator sits a little above the top of the ring, centered on it. Disabled, the ring keeps its fill, and its outline fades with the active track.
A design system can draw the thumb its own way from the same tokens. With an outline of 0 the thumb is a solid shape, and with a gap between the tracks and the thumb, the tracks stop short of it and are rounded there. m3-like draws Material’s upright bar that way.
Anatomy
Anatomy
continuous, as the web package draws it.| Part | Description |
|---|---|
1 track | The line the handle travels along: the whole range from min to max. Drawn in the track color where it is not active. |
2 active-track | The part of the track from the origin to the value, or between the two handles of a range, drawn over the track. |
3 handle | The part the person drags, one per value: in Tanrei a circle (its width along the track equals its height across it) with a fill and an outline of its own, a narrow upright bar where a system says so. |
4 handle-outline · optional | The ring drawn over the handle's fill, inside its edge, so the handle keeps its size however wide the ring is: a wider ring leaves a smaller disc of fill. None where its width is 0. |
5 container | The round state layer centered on a handle, moving with it. It carries hover, focus and press and is invisible at rest. |
6 stop-indicator · optional | A dot on the track's midline, half the track's thickness across in Tanrei: at an end of the handle's travel that lies on the track, and at every step while stops is true. |
7 value-indicator · optional | A small label beside a handle that shows its value while the handle is pressed or focused, or always. |
8 focus-ring | Gap-then-ring indicator around the handle that has keyboard focus. |
Measurements
Measurements
| Length | Token | Value (px) |
|---|---|---|
handle height | tanrei.comp.slider.handle.height.small | 20 |
container size | tanrei.comp.slider.container.size | 40 |
continuous, as the web package draws it.Properties
| Property | Type | Default | Description |
|---|---|---|---|
size | small | medium | large | "small" | The track's weight and the stop dots' size (and, where a system says so, the handle's height). Small is the everyday slider; medium and large give a prominent control (a volume or brightness slider) more weight. The hit area always meets the minimum target. A design system may add values. |
orientation | horizontal | vertical | "horizontal" | Horizontal runs from the inline start (min) to the inline end (max); vertical runs from the bottom (min) to the top (max). |
value | number | 0 | The value of the only handle, or of a range's end handle. |
startValue | number | 0 | The value of a range's start handle. Ignored while range is false. |
range | boolean | false | Draws two handles, choosing the range from startValue to value (a price filter). |
min | number | 0 | The lowest value, at the track's start. |
max | number | 100 | The highest value, at the track's end. |
step | number | 0 | 0 is continuous. Above 0 the slider is discrete: its values are min plus whole multiples of step, up to max. |
origin | number | null | Where the active track starts. Null starts it at min. A value inside the range makes a centered slider (a balance control) whose active track grows toward either side. Ignored while range is true. |
stops | boolean | false | Draws a stop dot at every value a discrete slider can take. Meant for a few steps; ignored while step is 0. |
valueIndicator | none | active | always | "none" | Whether a handle shows its value beside it: never, while it is pressed or has keyboard focus, or all the time. |
disabled | boolean | false | Prevents interaction and removes the handles from the focus order. |
label | string | "" | Names the slider (a range's end handle) for assistive technology. Never drawn: a visible label is the product's text next to the slider, in the same words. Required. |
startLabel | string | "" | Names a range's start handle ("Minimum price"). Required while range is true. |
valueText | string | "" | The value as words ("40 %", "3 stars"), spoken and shown by the value indicator in place of the number. For a range, the end handle's. |
startValueText | string | "" | The start handle's valueText, for a range. |
Events
| Event | Description |
|---|---|
input | The person changed a value; carries value and, for a range, startValue. Emitted for every change during a drag, not when the product sets a value. |
change | The person finished changing a value (the press ended, or a key or assistive-technology adjustment was made); carries value and, for a range, startValue. Always after the input it follows. |
States
Behavior
Requirements in EARS form. Every platform implementation is tested against these ids.
| ID | Requirement |
|---|---|
SLD-1 | THE slider SHALL keep value (and, while range is true, startValue) between min and max; WHILE step is greater than 0 it SHALL take only min plus whole multiples of step that do not exceed max, snapping to the nearest, and WHILE step is 0 any value between min and max. |
SLD-2 | WHEN the person presses the slider by pointer or touch — the track, a handle, or the space around them — THE slider SHALL move the handle nearer the press (for a range whose handles meet, the one that can move toward the press) to the value under it and, WHILE the press moves, SHALL make the handle follow it along the track, clamped to the handle's range and snapped to step; it SHALL emit input once for each value change and, WHEN the press ends with a value other than the one it started with, change once after the last input. On touch the handle SHALL move once the press has travelled along the track by the platform's touch slop, or when a press that did not move ends; a press that first travels across the track by the slop SHALL be left to an enclosing scroll view and SHALL change nothing. |
SLD-3 | WHILE a handle has keyboard focus THE slider SHALL increase its value by one keyboard step with Right Arrow or Up Arrow and decrease it with Left Arrow or Down Arrow (Right and Left swapped in a right-to-left horizontal slider), by ten keyboard steps with Page Up and Page Down, and set it to the lowest and highest value it can take with Home and End; the keyboard step is step, or one hundredth of max − min while step is 0. Each key press that changes the value SHALL emit input and then change once; a key press that changes nothing SHALL emit nothing. |
SLD-4 | WHEN assistive technology increments or decrements a handle (a vertical swipe on an adjustable element, the platform's increment and decrement actions) THE slider SHALL change its value by one keyboard step, and WHEN it sets a value directly where the platform allows that THE slider SHALL clamp and snap it; either SHALL emit input and then change once. |
SLD-5 | WHILE range is true THE slider SHALL draw a start handle at startValue and an end handle at value, SHALL stop each handle at the other's value so that the two may meet but never cross, and SHALL make each handle its own Tab stop, the start handle first, and its own element for assistive technology. |
SLD-6 | WHILE range is false THE slider SHALL draw the active track between origin (min while origin is null, and origin clamped to the range) and value, on whichever side of origin value lies; WHILE range is true it SHALL draw the active track between startValue and value. |
SLD-7 | THE slider SHALL be as long along its axis as its layout gives it and, across it, as thick as the larger of comp.slider.container.size and comp.slider.handle.height.small for size small (and so on for each size); it SHALL extend its hit area across its axis to sys.target.touch for touch input and sys.target.pointer for fine pointers without changing that size (a platform that cannot tell the input type SHALL use sys.target.touch), the whole slider being the hit area, and a handle drawn smaller than the target SHALL NOT make the hit area smaller. It SHALL draw the track comp.slider.track.thickness.small thick for size small (and so on), centered across the axis and inset from each end of the slider by half of comp.slider.container.size, so that a handle and its state layer stay inside the slider at min and max; a handle's center SHALL travel between the two points half the track's thickness in from the track's ends, concentric with its rounded ends. |
SLD-8 | THE slider SHALL draw the track in comp.slider.standard.track.color and the active track over it in comp.slider.standard.active-track.color. WHILE comp.slider.track.gap is 0 the track and the active track SHALL run under each handle and meet at its center, so that nothing lies between the active track and the handle; WHILE it is above 0 they SHALL stop that far from each side of each handle, measured from the handle's edge. The pieces SHALL be rounded by comp.slider.track.shape.small for size small (and so on), clamped to half the thickness, at the track's two ends and, WHILE comp.slider.track.gap is above 0, beside a handle; they SHALL be square where two pieces meet (under a handle while the gap is 0, and at an origin inside the travel). It SHALL draw each handle centered on the track's midline, comp.slider.standard.handle.width along the axis and comp.slider.handle.height.small across it for size small (and so on), rounded by comp.slider.handle.shape clamped to half its smaller side, painted as SLD-24 says, above the tracks — each part from the tokens of the state SLD-11 names. |
SLD-9 | THE slider SHALL draw a dot of comp.slider.stop-indicator.size.small for size small (and so on) centered on the track's midline at each end of the handles' travel that lies on the track rather than on the active track, and WHILE stops is true and step is greater than 0 also at every value the slider can take; a dot SHALL be drawn in comp.slider.standard.stop-indicator.color where it lies on the track and in comp.slider.standard.active-stop-indicator.color where it lies on the active track, SHALL NOT be drawn where a handle or its track gap covers it, and a size of 0 SHALL draw none. |
SLD-10 | THE slider SHALL draw each handle's container, a shape of comp.slider.container.size on each side with corners comp.slider.container.shape, centered on the handle and moving with it, filled with comp.slider.standard.container.color from the handle's state, behind the handle and above the tracks; it may extend beyond the track without affecting layout, and a size of 0 SHALL draw none. |
SLD-11 | WHILE a handle is hovered, keyboard-focused or pressed THE slider SHALL render that handle and its container from the component tokens of that handle's highest-precedence active state, with precedence pressed over focused over hovered, and the tracks, stop dots and opacity from the highest-precedence state of any of its handles; a handle is hovered while a fine pointer is over the slider and that handle is the one a press there would move; focus gained by pointer or touch SHALL NOT select the focused state. WHEN the colors change between states THE slider SHALL animate them over comp.slider.motion.state-duration with comp.slider.motion.state-easing, and at once where motion is reduced. |
SLD-12 | WHEN a handle receives keyboard focus THE slider SHALL show the focus ring around that handle without animation; focus gained by pointer or touch SHALL NOT show it. |
SLD-13 | WHEN the slider is pressed by pointer or touch THE slider SHALL show the pressed state of the handle the press moves without delay, SHALL keep it for as long as the press lasts, and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner. |
SLD-14 | WHILE pressed THE slider SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; handle, tracks, stop dots, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.slider.motion.feedback-duration with comp.slider.motion.state-easing, fades over comp.slider.motion.state-duration with comp.slider.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced. The slider's point of contact is the pressed handle's center, because the handle moves to the press. |
SLD-15 | THE slider SHALL ease its container, handle and track corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show its keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README. |
SLD-16 | WHEN a value changes by a key, by assistive technology, by a press on the track that did not drag, or because the product set it, THE slider SHALL move the handle and the active track to it over comp.slider.motion.value-duration with comp.slider.motion.value-easing; WHILE a press drags a handle, the handle SHALL follow the press without delay; WHERE motion is reduced every change SHALL be shown at once. WHEN the product sets value or startValue THE slider SHALL NOT emit input or change. |
SLD-17 | WHILE valueIndicator is always, or valueIndicator is active and a handle is pressed or has keyboard focus, THE slider SHALL draw that handle's value indicator comp.slider.value-indicator.offset beyond the handle's outer edge across the axis — above the handle in a horizontal slider, at its inline start in a vertical one; for a round handle beyond the top of its circle, outline included, which lies half of comp.slider.handle.height.small from the track's midline for size small (and so on) — with its center on the handle's center along the axis, shifted along the axis only as far as it must to stay inside the window: the handle's value text on one line in comp.slider.value-indicator.label.typescale and comp.slider.value-indicator.label.color, centered in a container of comp.slider.value-indicator.container.color at least comp.slider.value-indicator.container.height high and as wide as the text plus comp.slider.value-indicator.container.padding-inline on each side but never narrower than it is high, rounded by comp.slider.value-indicator.container.shape. The value indicator SHALL be drawn above neighbouring content and above the handle and its container, without affecting layout or being clipped by the slider, SHALL take no focus and no input (a read overlay), SHALL be hidden from assistive technology, and SHALL fade in and out over comp.slider.motion.state-duration with comp.slider.motion.state-easing, at once where motion is reduced. |
SLD-18 | THE slider SHALL give each handle a value text: valueText for the only handle and a range's end handle, startValueText for a range's start handle; or, while that is empty, what the product's own function writes for that handle's value, WHERE the product gave the slider such a function in the platform's idiom; or, with neither, the value formatted as a number in the platform's locale with as many fraction digits as the keyboard step has, at most two; the value indicator SHALL show it and assistive technology SHALL read it as the handle's value. THE slider SHALL ask the function again at every change of a value and WHEN the function itself changes, SHALL take an empty answer as no answer, and SHALL announce nothing for a change of the words alone. |
SLD-19 | THE slider SHALL expose each handle as a slider (the adjustable trait on Apple platforms) named by label, or by startLabel for a range's start handle, with its value, its value text, the lowest and highest value it can take (for a range, bounded by the other handle), its orientation and its disabled state; the tracks, stop dots, container and value indicator SHALL be hidden from assistive technology. WHILE a handle's label (label, or startLabel for a range's start handle) is not empty THE slider SHALL expose it as that handle's accessible name, over a name the product gives the slider on the element (aria-label or aria-labelledby). WHILE label is empty and the product gives the slider a name of its own on the element — aria-label, or aria-labelledby naming an element — THE slider SHALL expose that name on each handle whose own label is empty. WHILE label is empty and the product gives it none, or range is true and startLabel is empty, THE slider SHALL still render, and in debug builds report that a handle has no accessible name: one name of the product's does not tell a range's two handles apart. |
SLD-20 | WHILE disabled is true THE slider SHALL NOT change a value or emit input or change, SHALL NOT be focusable, SHALL show no value indicator, SHALL expose the disabled state to assistive technology, and SHALL render its container, handles, tracks, stop dots and opacity from the disabled component tokens. |
SLD-21 | WHILE orientation is vertical THE slider SHALL run its track from the bottom (min) to the top (max), be as tall as its layout gives it, and lay out every part SLD-7 to SLD-17 name turned a quarter turn; WHEN the layout direction is right-to-left THE slider SHALL run a horizontal track from the right (min) to the left (max), mirroring the active track, the stop dots and the arrow keys (SLD-3), and SHALL NOT mirror a vertical slider. WHILE a value indicator is drawn in a right-to-left layout THE slider SHALL keep it centred on its handle and shift it along the axis only as far as it must to stay inside the window, away from whichever edge of the window it would cross, as in left-to-right (SLD-17): the shift is physical and is not mirrored. |
SLD-22 | IF value, startValue or origin lies outside min and max, or off a step, THE slider SHALL draw and expose it clamped and snapped without emitting input or change; IF startValue is greater than value while range is true THE slider SHALL draw both handles at value; IF max is not greater than min THE slider SHALL draw its handles at the track's start and take no input; IF step is below 0 THE slider SHALL treat it as 0. In debug builds it SHALL report each of these. |
SLD-23 | WHEN size names a value that the active design system does not have, or orientation or valueIndicator a value the component does not define, THE slider SHALL render with the default value (small, horizontal, none), and in debug builds report the unknown value. |
SLD-24 | THE slider SHALL fill the whole shape of each handle with comp.slider.standard.handle.color and draw the handle's outline over the fill, comp.slider.handle-outline.width wide inside the handle's edge, in comp.slider.standard.handle-outline.color, so that the outline never changes the handle's width or height and a width of 0 draws none; it SHALL raise the handle by comp.slider.standard.handle.elevation. The parts under a handle (the tracks where they meet, a stop dot, the container) SHALL show through it only as far as its fill is translucent. |
SLD-25 | THE slider SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the slider is shown THE slider SHALL draw itself for the new direction at once, without a motion of its own, keeping its values and its focus, each value indicator placed again by the new direction. |
SLD-26 | THE slider SHALL lay out a handle's value text in its value indicator as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE slider SHALL still place the indicator and align its lines by the layout direction, not by the text's own. |
SLD-27 | WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE slider SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries its role (every handle carries them) — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the slider is invalid — except those the slider owns (aria-disabled), which it SHALL move and not expose. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE slider SHALL take it off that element. WHEN the product sets an aria- attribute the slider owns THE slider SHALL report it in debug builds. |
Platform name mapping
Parity does not mean identical APIs. Each platform uses its own idiom for the same contract.
| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
|---|---|---|---|---|---|
| Component | tanrei-slider | TanreiSlider | TanreiSlider | TanreiSlider | Slider |
size · Property | size | size | size | size | size |
orientation · Property | orientation | orientation | orientation | orientation | orientation |
value · Property | value | value | value | value | value |
startValue · Property | startValue | value | value | start | startValue |
range · Property | range | value | value | range | range |
min · Property | min | bounds | valueRange | min | minimumValue |
max · Property | max | bounds | valueRange | max | maximumValue |
step · Property | step | step | step | step | step |
origin · Property | origin | origin | origin | origin | origin |
stops · Property | stops | showsStops | showStops | showStops | stops |
valueIndicator · Property | valueIndicator | valueIndicator | valueIndicator | valueIndicator | valueIndicator |
disabled · Property | disabled | disabled | enabled | onChanged | disabled |
label · Property | label | label | contentDescription | semanticLabel | accessibilityLabel |
startLabel · Property | startLabel | startLabel | startContentDescription | startSemanticLabel | startAccessibilityLabel |
valueText · Property | valueText | valueText | stateDescription | semanticValue | valueText |
startValueText · Property | startValueText | startValueText | startStateDescription | startSemanticValue | startValueText |
input · Event | input | value | onValueChange | onChanged | onValueChange |
change · Event | change | onCommit | onValueChangeFinished | onChangeEnd | onSlidingComplete |
| Notes | Attributes are kebab-case (start-value, value-indicator, start-label, value-text, start-value-text). Each handle is an element with role="slider" and its aria-value attributes, aria-orientation and aria-label in the shadow root (not ElementInternals, which axe cannot see). Not <input type="range">: it has one handle, no gap, no stop dots, and its parts cannot be styled alike in every engine. The handle's outline is its box's border with box-sizing: border-box, over its background (the fill), so it keeps its width and height (SLD-24); while track.gap is 0 the two pieces of the tracks meet at the handle's center under it (SLD-8). Form-associated: it takes name and submits value (a range submits startValue, then value, under the same name), and it sets its own value and startValue before firing input and change, as a native control does. Snapshot harness: the stages of SLD-7. The product's function for a value's words (SLD-18) is formatValue, a property and never an attribute: ((value: number) => string) | null, null by default; it is asked for each handle at every change of a value and when the property itself is set. | Slider idiom: TanreiSlider(_ label:, value: Binding<Double>, in bounds: ClosedRange<Double> = 0...100, step:); a range is the initializer whose value is a Binding<ClosedRange<Double>> (lower bound startValue, upper bound value) and which also takes startLabel. Every change writes the binding, which is the input event. onCommit is Tanrei's own parameter, called when a change is finished (SLD-2, SLD-3, SLD-4), as the text field's is; SwiftUI's onEditingChanged reports a drag's start and end, not a change. SwiftUI's own Slider is not used: its track and handle cannot take the tokens. Each handle is an adjustable element (.accessibilityAdjustableAction, .accessibilityValue); disabled follows .disabled(_:). Lagging: ships an earlier form of the slider — a solid round handle, tracks cut at the handle's edge (square there while track.gap is 0), one stop-dot size for every size. It does not draw yet: the handle's fill and outline (SLD-24), the tracks meeting under the handle while track.gap is 0 (SLD-8), the stop dot per size (SLD-9) and the value indicator's placement over a round handle (SLD-17). Not built on this platform yet: the product's function for a value's words (SLD-18) is a format: ((Double) -> String)? = nil parameter. Not built on this platform yet: the stage color a fixture's surface names, which the continuous-on-container-highest fixture draws on. | Compose idiom (as Slider and RangeSlider): TanreiSlider(value: Float, onValueChange, modifier, enabled, valueRange: ClosedFloatingPointRange<Float> = 0f..100f, step: Float = 0f, onValueChangeFinished, …); a range is the overload whose value is a ClosedFloatingPointRange<Float> (start and end) and which also takes startContentDescription and startStateDescription. step is a size, not Material's count of steps. State hoisting: the caller moves value from onValueChange. enabled is the inverse flag. Each handle has Compose's slider semantics (progressBarRangeInfo and setProgress), with stateDescription as its value text. Lagging: ships an earlier form of the slider — a solid round handle with its shape clipped out of the tracks, one stop-dot size for every size. It does not draw yet: the handle's fill and outline (SLD-24), the tracks meeting under the handle while track.gap is 0 (SLD-8), the stop dot per size (SLD-9) and the value indicator's placement over a round handle (SLD-17). Not built on this platform yet: the product's function for a value's words (SLD-18) is a format: ((Float) -> String)? = null parameter. Not built on this platform yet: the stage color a fixture's surface names, which the continuous-on-container-highest fixture draws on. | Flutter idiom (as Slider): TanreiSlider(value:, onChanged:, onChangeEnd:, min: 0, max: 100, step: 0, …), with step a size rather than Material's divisions; a range is the named constructor TanreiSlider.range(start:, end:, onChanged: (start, end) {…}, …), since Flutter's RangeValues belongs to Material. A null onChanged disables it. orientation is Flutter's Axis. Each handle is a Semantics(slider: true, value:, increasedValue:, decreasedValue:, onIncrease:, onDecrease:) node; the widgets library draws everything else. Lagging: ships an earlier form of the slider — a solid round handle, tracks cut at the handle's edge and rounded there (a notch between the round end and the handle), one stop-dot size for every size. It does not draw yet: the handle's fill and outline (SLD-24), the tracks meeting under the handle while track.gap is 0 (SLD-8), the stop dot per size (SLD-9) and the value indicator's placement over a round handle (SLD-17). Not built on this platform yet: the product's function for a value's words (SLD-18) is format: String Function(double)?. Not built on this platform yet: the stage color a fixture's surface names, which the continuous-on-container-highest fixture draws on. | Controlled, in the community slider's idiom (value, minimumValue, maximumValue, step, onValueChange, onSlidingComplete), without depending on it: React Native has no slider in its core. A range adds range and startValue, and both callbacks receive (value, startValue). Each handle is a view with accessibilityRole="adjustable", accessibilityValue ({ min, max, now, text }) and the increment and decrement accessibility actions; the drag is a the touch responder props (onStartShouldSetResponder and the rest) that yields to an enclosing ScrollView as SLD-2 says. The arrow keys work where a hardware keyboard is attached, as the tabs row's do. Lagging: ships an earlier form of the slider — a solid round handle, tracks cut at the handle's edge and rounded there (a notch between the round end and the handle), one stop-dot size for every size. It does not draw yet: the handle's fill and outline (SLD-24), the tracks meeting under the handle while track.gap is 0 (SLD-8), the stop dot per size (SLD-9) and the value indicator's placement over a round handle (SLD-17). Not built on this platform yet: the product's function for a value's words (SLD-18) is a formatValue prop, (value: number) => string. Not built on this platform yet: the stage color a fixture's surface names, which the continuous-on-container-highest fixture draws on. |
Tokens
| Token | Aliases | Resolved (light) |
|---|---|---|
--tanrei-comp-slider-container-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-slider-container-size | — | 40px |
--tanrei-comp-slider-disabled-opacity | — | 1 |
--tanrei-comp-slider-focused-opacity | — | 1 |
--tanrei-comp-slider-handle-outline-width | — | 3px |
--tanrei-comp-slider-handle-height-large | — | 20px |
--tanrei-comp-slider-handle-height-medium | — | 20px |
--tanrei-comp-slider-handle-height-small | — | 20px |
--tanrei-comp-slider-handle-shape | sys.shape.corner.full | 9999px |
--tanrei-comp-slider-hovered-opacity | — | 1 |
--tanrei-comp-slider-motion-feedback-duration | sys.motion.role.feedback | 450ms |
--tanrei-comp-slider-motion-state-duration | sys.motion.role.state.compact | 200ms |
--tanrei-comp-slider-motion-state-easing | sys.motion.easing.state | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-slider-motion-value-duration | sys.motion.role.selection.travel | 250ms |
--tanrei-comp-slider-motion-value-easing | sys.motion.easing.spatial | cubic-bezier(0.2, 0, 0, 1) |
--tanrei-comp-slider-pressed-opacity | — | 1 |
--tanrei-comp-slider-standard-active-stop-indicator-color | sys.color.on-primary | |
--tanrei-comp-slider-standard-active-track-color | sys.color.primary | |
--tanrei-comp-slider-standard-container-color | — | |
--tanrei-comp-slider-standard-disabled-active-stop-indicator-color | — | |
--tanrei-comp-slider-standard-disabled-active-track-color | — | |
--tanrei-comp-slider-standard-disabled-container-color | — | |
--tanrei-comp-slider-standard-disabled-handle-outline-color | — | |
--tanrei-comp-slider-standard-disabled-handle-color | — | |
--tanrei-comp-slider-standard-disabled-handle-elevation | — | |
--tanrei-comp-slider-standard-disabled-handle-width | — | 20px |
--tanrei-comp-slider-standard-disabled-state-layer-color | — | |
--tanrei-comp-slider-standard-disabled-stop-indicator-color | — | |
--tanrei-comp-slider-standard-disabled-track-color | — | |
--tanrei-comp-slider-standard-focused-active-stop-indicator-color | — | |
--tanrei-comp-slider-standard-focused-active-track-color | — | |
--tanrei-comp-slider-standard-focused-container-color | — | |
--tanrei-comp-slider-standard-focused-handle-outline-color | — | |
--tanrei-comp-slider-standard-focused-handle-color | — | |
--tanrei-comp-slider-standard-focused-handle-elevation | — | |
--tanrei-comp-slider-standard-focused-handle-width | — | 20px |
--tanrei-comp-slider-standard-focused-state-layer-color | — | |
--tanrei-comp-slider-standard-focused-stop-indicator-color | — | |
--tanrei-comp-slider-standard-focused-track-color | — | |
--tanrei-comp-slider-standard-handle-outline-color | sys.color.primary | |
--tanrei-comp-slider-standard-handle-color | sys.color.surface | |
--tanrei-comp-slider-standard-handle-elevation | sys.elevation.level0 | |
--tanrei-comp-slider-standard-handle-width | — | 20px |
--tanrei-comp-slider-standard-hovered-active-stop-indicator-color | — | |
--tanrei-comp-slider-standard-hovered-active-track-color | — | |
--tanrei-comp-slider-standard-hovered-container-color | — | |
--tanrei-comp-slider-standard-hovered-handle-outline-color | — | |
--tanrei-comp-slider-standard-hovered-handle-color | — | |
--tanrei-comp-slider-standard-hovered-handle-elevation | — | |
--tanrei-comp-slider-standard-hovered-handle-width | — | 20px |
--tanrei-comp-slider-standard-hovered-state-layer-color | — | |
--tanrei-comp-slider-standard-hovered-stop-indicator-color | — | |
--tanrei-comp-slider-standard-hovered-track-color | — | |
--tanrei-comp-slider-standard-pressed-active-stop-indicator-color | — | |
--tanrei-comp-slider-standard-pressed-active-track-color | — | |
--tanrei-comp-slider-standard-pressed-container-color | — | |
--tanrei-comp-slider-standard-pressed-handle-outline-color | — | |
--tanrei-comp-slider-standard-pressed-handle-color | — | |
--tanrei-comp-slider-standard-pressed-handle-elevation | — | |
--tanrei-comp-slider-standard-pressed-handle-width | — | 20px |
--tanrei-comp-slider-standard-pressed-state-layer-color | — | |
--tanrei-comp-slider-standard-pressed-stop-indicator-color | — | |
--tanrei-comp-slider-standard-pressed-track-color | — | |
--tanrei-comp-slider-standard-state-layer-color | comp.slider.standard.handle-outline.color | |
--tanrei-comp-slider-standard-stop-indicator-color | sys.color.on-surface-variant | |
--tanrei-comp-slider-standard-track-color | sys.color.surface-variant | |
--tanrei-comp-slider-stop-indicator-size-large | — | 4px |
--tanrei-comp-slider-stop-indicator-size-medium | — | 3px |
--tanrei-comp-slider-stop-indicator-size-small | — | 2px |
--tanrei-comp-slider-track-gap | sys.space.space0 | 0px |
--tanrei-comp-slider-track-shape-large | sys.shape.corner.full | 9999px |
--tanrei-comp-slider-track-shape-medium | sys.shape.corner.full | 9999px |
--tanrei-comp-slider-track-shape-small | sys.shape.corner.full | 9999px |
--tanrei-comp-slider-track-thickness-large | — | 8px |
--tanrei-comp-slider-track-thickness-medium | — | 6px |
--tanrei-comp-slider-track-thickness-small | — | 4px |
--tanrei-comp-slider-value-indicator-container-color | sys.color.inverse-surface | |
--tanrei-comp-slider-value-indicator-container-height | — | 24px |
--tanrei-comp-slider-value-indicator-container-padding-inline | sys.space.space100 | 8px |
--tanrei-comp-slider-value-indicator-container-shape | sys.shape.corner.small | 6px |
--tanrei-comp-slider-value-indicator-label-color | sys.color.inverse-on-surface | |
--tanrei-comp-slider-value-indicator-offset | sys.space.space100 | 8px |
--tanrei-comp-slider-value-indicator-label-typescale | sys.typescale.label-small | 500 12px/1.3333 |
Fixtures
Canonical renderings. Every platform snapshots each fixture under the same id so the results can be compared.
size: small (9)
continuous
value: 40 label: Volume continuous-on-container-highest
value: 40 label: Volume discrete-stops
value: 30 step: 10 stops: true label: Brightness range
value: 60 startValue: 20 range: true label: Maximum price startLabel: Minimum price centered
value: 20 min: -50 max: 50 origin: 0 label: Balance value-indicator
value: 40 valueIndicator: always label: Volume vertical
orientation: vertical value: 60 label: Volume disabled
value: 40 disabled: true label: Volume disabled-stops
value: 60 step: 20 stops: true disabled: true label: Brightness size: medium (1)
medium
size: medium value: 40 label: Volume size: large (1)
large
size: large value: 40 label: Volume Generated from specs/slider.spec.json, tokens/comp/slider.tokens.json, design/tokens/
Use a slider when people choose a value by feel and see its effect as they move: volume, brightness, a photo’s exposure, a map’s search radius. A range slider suits a filter with a low and a high bound, such as a price.
When the exact number matters more than its position, such as an amount of money or a quantity, use a text field, or put one next to the slider so the person can do either. When there are only a few named choices, such as Low, Medium and High, radio buttons name each one, or a segmented button where the system ships one.
- Continuous (
step0) takes any value betweenminandmax. Use it when the in-between values are meaningful and the effect shows at once. - Stepped (
stepabove 0) snaps tominplus whole multiples ofstep. Use it when only whole units make sense: stars, a font size in points, a percentage in tens. stopsdraws a dot at every value a stepped slider can take. Turn it on when there are few steps, so people can count them. With many steps the dots crowd the track, so leave them off.
The arrow keys move by one step, or by one hundredth of the range on a continuous slider. Page Up and Page Down move by ten.
range draws two handles, from startValue to value. The handles can meet but never cross. Each is its own Tab stop and its own control for a screen reader, which hears the range the handle can still reach. Name both handles: startLabel (“Minimum price”) and label (“Maximum price”).
origin sets where the active track starts. Left empty, it starts at min. A value inside the range, such as 0 on a balance control from -50 to 50, makes the active track grow from there toward either side. The ends of the track keep their stop dots, so both limits stay visible.
valueIndicator shows the handle’s value in a small label beside it.
| Value | Shows the value | Use for |
|---|---|---|
none (default) |
Never | Sliders whose effect shows elsewhere on screen: volume, brightness |
active |
While the handle is pressed or has keyboard focus | Values people need to read while they set them, where a finger covers the handle |
always |
All the time | A value that must stay readable, when there is no room for your own text beside the slider |
The indicator sits above a horizontal slider’s handle and at the inline start of a vertical one. It shows valueText when you give one, and otherwise the number in the platform’s locale, with as many decimal places as the keyboard step has, up to two. It is only a view of the value: it takes no focus and no input, and a screen reader reads the value from the handle instead.
Set valueText to the words for the current value when the bare number would be unclear: “40%”, “3 stars”, “$300”. It replaces the number both in the value indicator and in what a screen reader says. For a range, startValueText does the same for the start handle. Update it whenever the value changes.
Words written once are wrong after the first drag. So the slider also takes one function from a value to its words, given once: it asks the function for each handle at every change, and valueText and startValueText still win where you set them. On the web it is formatValue, a property and never an attribute. The other platforms have not implemented it yet.
const dollars = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 });
document.querySelector('[name="price"]').formatValue = (value) => dollars.format(value);Used in the slider field: a field and a slider as one component, with these words drawn at the end of the label row. Use it wherever the slider needs a label that is drawn, help or an error.
small is the everyday slider. medium and large give a prominent control, such as a volume or brightness slider on a media screen, a heavier track. Every size meets the minimum target.
A vertical slider runs from the bottom (min) to the top (max) in every layout direction. A horizontal slider runs from the inline start, so in a right-to-left layout min is on the right.
- Put your own visible label beside the slider, in the same words as
label. - Show the effect of the value while the person drags, and save it on
change, not on everyinput. - Choose
min,maxandstepso that every value the slider can take is one people would want. - Give
valueTexta unit when the number alone does not say what it counts.
- Don’t use a slider for a value people must set precisely, such as a date or a sum of money.
- Don’t use a slider for a setting that is only on or off. Use a switch.
- Don’t show stop dots on a slider with many steps.
- Don’t put a slider inside a toolbar. It needs the arrow keys the toolbar moves focus with.
- Role
slider- Labeling
- label names the slider, or a range's end handle; startLabel names a range's start handle. Neither is drawn: a visible label is the product's text beside the slider, in the same words. valueText replaces the spoken number ("40 %", "3 stars"). The tracks, stop dots, state layer and value indicator are decorative.
- Minimum target
- The hit area meets the minimum target size even when the visual size is smaller. minimum 32px · pointer 44px · spacing 8px · touch 48px
Keyboard
| Keys | Action |
|---|---|
| Tab / Shift+Tab | Moves focus to the handle, and for a range to the start handle, then the end handle. |
| Right Arrow / Up Arrow | Increases the focused handle's value by one keyboard step (Right Arrow decreases it in a right-to-left horizontal slider). |
| Left Arrow / Down Arrow | Decreases the value by one keyboard step (Left Arrow increases it in a right-to-left horizontal slider). |
| Page Up / Page Down | Increases or decreases the value by ten keyboard steps. |
| Home / End | Sets the value to the lowest or highest the handle can take. |
Notes
- Each handle is one slider for assistive technology: role slider with aria-valuemin, aria-valuemax, aria-valuenow, aria-valuetext and aria-orientation on the web; the adjustable trait with an accessibility value on Apple platforms (a vertical swipe increments and decrements); a range with a current value and the set-progress action on Android; a slider semantics node with increase and decrease actions in Flutter; role adjustable with accessibilityValue and increment and decrement actions in React Native.
- A range is two sliders, the WAI-ARIA multi-thumb pattern: each handle's lowest and highest value is bounded by the other handle, so a screen reader announces the range each can still reach.
- The value is read from the handle and the active track. The handle must be told apart from each thing it lies on — the track, the active track and the surface behind the slider — by its outline (comp.slider.standard.handle-outline.color, where comp.slider.handle-outline.width is above 0) or by its fill (comp.slider.standard.handle.color), at 3:1 or more; comp.slider.standard.active-track.color must stay at least 3:1 against the track and the surface. In the default system the outline carries it against the track and the surface, and the fill against the active track. A faint track keeps its end visible by the stop dot at the end of the travel (SLD-9).
- The handle may be drawn smaller than the minimum target (20 against 48 in the default system): the whole slider takes the press across its axis at the target's size (SLD-7), so every handle keeps its whole target, a range's two handles included, and no target is reported larger than the area that takes the press.
- Text size does not change the slider: it has no text of its own except the value indicator, which grows with the text.
- How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.
Generated from specs/slider.spec.json, tokens/sys/measure.tokens.json
- Each handle is a slider for assistive technology, named by
label(orstartLabelfor a range’s start handle), with its value, its value text, the lowest and highest value it can take, its orientation and its disabled state. On Apple platforms it is an adjustable element: a vertical swipe moves it one step. - A range is two sliders, the WAI-ARIA multi-thumb pattern. Each handle’s limits are bounded by the other handle, so a screen reader announces the range it can still reach.
- The tracks, stop dots, state layer and value indicator are hidden from assistive technology.
- Each handle is a Tab stop, the start handle first. Arrow keys move one step, Page Up and Page Down ten steps, and Home and End to the lowest and highest value. In a right-to-left horizontal slider, Right and Left Arrow are swapped.
- A screen reader’s increment and decrement actions move one step; setting a value directly, where the platform allows it, is clamped and snapped.
- A press anywhere on the slider moves the nearer handle there, and a drag follows. On touch, a press that first moves across the track is left to the scroll view around it, so the slider does not catch a scroll.
- The whole slider is the hit area, and it reaches the minimum target across the track. The thumb is drawn smaller than the target, but that does not make the hit area smaller, so each handle of a range keeps the whole target too.
- It shows the focus indicator around the focused handle for keyboard focus only.
- The handle is told apart from everything it lies on at 3:1 or more in the default system: by its outline against the track and the page, and by its fill against the active track. The active track keeps 3:1 against the track and the page, and the end of a faint track stays visible by its stop dot.
- A disabled slider is not focusable, shows no value indicator and is exposed as disabled.
- Text size changes only the value indicator, which grows with the text.
- Debug builds report a missing
labelorstartLabel, a value, start value or origin outside the range or off a step, a start value above the value, amaxnot abovemin, a negative step, and an unknown size, orientation or value-indicator setting.
Known platform gaps:
- On the web each handle has the slider role in the shadow root, not through
ElementInternals, because axe-core cannot see semantics set that way. It is not an<input type="range">, which has one handle and parts no two engines style alike. - Web: assistive technology adjusts an ARIA slider through the arrow keys the browser sends it, so a value cannot be set directly.
- Web: the value indicator is not in the top layer. An ancestor with
overflow: hiddenclips it, and a later positioned sibling with az-indexabove 1 covers it. - SwiftUI cannot expose a handle’s lowest and highest value or its orientation, and on macOS a handle is not a slider for assistive technology.
- SwiftUI cannot tell a touch from a pointer: on iOS every press is a touch, an iPad pointer’s too, and on macOS every press is a pointer’s. Before iOS 18 a touch’s drag is a SwiftUI gesture, which a scroll view around the slider may not be able to take over.
- SwiftUI: the value indicator is kept inside the slider rather than the window, and a view drawn after the slider can cover it.
- SwiftUI: the keys are tested as rules, not yet with a real keyboard, because the simulator gives no keyboard focus under automation.
- Compose’s slider semantics have no orientation, so a handle’s orientation is not exposed.
- Flutter exposes no lowest or highest value and no orientation for a handle, and no action to set a value directly.
- Flutter always writes the number with a
.as the decimal separator, in every locale. SetvalueTextwhere that matters. - Flutter: the hit area grows across the track only where the slider’s parent is larger than the slider. A parent exactly the slider’s size cuts it off.
- React Native has no action to set a value directly and no orientation property, so neither is exposed.
- On React Native the arrow keys work only where a hardware keyboard is attached, as in the tabs row.
- React Native treats every press as a touch: a mouse or stylus press waits until it has moved or ended before it moves the handle.
- React Native draws the value indicator inside the slider, not in the overlay layer, so it stays inside the slider rather than the window, and a view drawn after the slider can cover it.
- React Native: the layout, the drag and how it gives way to a scroll view, and the adjustable handles have not yet been checked on a device.
- SwiftUI, Compose, Flutter and React Native still draw the previous look: a solid handle, one stop-dot size for every size, and the tracks cut at the handle rather than meeting under it. Where they cut, they end the track differently: square on SwiftUI, rounded with a notch on Flutter and React Native, and cut to the handle’s shape on Compose.
- Under
cupertino, the white thumb on a white page measures 1.0:1 in the light schemes, below the 3:1 a part that shows the value needs. It has no outline, as Apple draws it, and is told apart by its shadow and by the tinted active track beside it; no invariant checks a handle against the page. The 1.0:1 is a measurement kept as information: Tanrei’s accessibility criteria do not apply to an imitation preset. Undercupertinothe slider also draws no stop dots, not even at the ends of the track. - Under
m3-like, the slider draws Material’s upright bar, from Material’s published measurements. They have not yet been checked against the fidelity reference. Underm3-expressive-like, the glyph Material draws inside a thick track has no place (mimicry gapslider-inset-icon): put your icon beside the slider.
- Give every slider a
label, and a range astartLabeltoo, in your product’s language. Show the same words as a visible label beside it. - Set
valueTextwhen the number alone is unclear, and keep it in step with the value. - Show the result of a change somewhere people can see it, and tell them in text if a change could not be applied. The slider announces only its own value.
- Under
cupertino, where the thumb is white and the track has no end dots, keep the slider off a plain white surface when you can. - On a surface of the highest container tone, such as a filled card, the inactive track is hard to see: it has nearly the same tone. Where a slider stands on one, point
comp.slider.standard.track.colorat a stronger role in a layer. The handle and the active track still show the value. - On the web, give the slider a
nameto submit its value with a form.