Sliders
components specs/components/sliders.kmd
Single-handle or range-handle control for selecting a numeric value along a continuous or discrete scale. Material parity (`/components/sliders`). Covers continuous vs discrete, single vs range, value label, step marks, and accessibility.
When this spec applies
Primary triggers
- Add a slider
All triggers
- Pick a value within a numeric range (volume, brightness)
- Pick a range of values (price range filter)
- Snap-to-step input (rating, level)
Specification body
Spec — Sliders
Facet Visual of Koder Design. Material parity: https://m3.material.io/components/sliders.
Variant matrix
| Continuous | Discrete | |
|---|---|---|
| Single | Smooth drag, any value | Drag snaps to steps |
| Range | Two handles, smooth | Two handles, snap to steps |
Pick:
- Continuous when any value within range is meaningful (volume, hue).
- Discrete when only certain values matter (rating 1-5, T-shirt size).
- Range when user picks an interval (price min-max, time of day range).
Anatomy (continuous single)
Volume 75
────●────────────────────────────────── ↑
↑ ↑ value label
track handle
- Track: 4 px tall (default), pill-shaped,
surface-container-highest - Active track (filled portion):
primarycolor - Handle: 20 px circle,
primarycolor, 4 px halo on focus / drag - Track gap: 4 px on each side of handle (visible separation; Material 3 detail)
- Value label: chip-shaped overlay above handle on drag
- Hit target: 48 × 48 px around handle (extends beyond visual 20 px)
R1 — Discrete with step marks
Rating ★ ★ ★ ★ ☆
──●──┊──┊──┊──┊──┊
1 2 3 4 5
- Step marks: 2 px circles on the track at each step position
- Marks under filled portion:
on-primarycolor (subtle) - Marks over unfilled portion:
on-surface-variant - Snap-to-step: handle releases to nearest step
- Step intervals must be uniform OR documented (e.g., powers-of-2)
R2 — Range slider
Two handles + filled segment between them:
Price $35 ──── $120
──────●━━━━━━━━━━━━━━●──────────
↑ ↑
min max
- Both handles draggable
- Min handle cannot pass max handle (and vice versa)
- Optional minimum gap between handles (e.g., min range = 5 units)
- Two value labels on drag
R3 — Value label
| When visible | Detail |
|---|---|
| Drag in progress | Always |
| Handle focused (keyboard) | Always |
| Handle hovered (mouse) | Optional (configurable) |
| Idle | Hidden (default); pinned-on opt-in |
Label content:
- Numeric value (default)
- Formatted value (e.g., "$35", "75%", "2 hr 30 min")
- Custom string via formatter function
Label shape:
- 28 px tall, pill-shaped
primarybg,on-primarytextlabel-medium(12/16, weight 500)- Anchored 12 px above handle; tip pointing down
R4 — States
| State | Visual |
|---|---|
| Rest | Default |
| Hovered (handle / track) | Subtle scale of handle to 24 px |
| Focused | 4 px focus ring outside handle |
| Dragging | Handle scale to 24 px + value label visible |
| Disabled | 38% opacity entire slider, no interaction |
R5 — Tick / step mark variants
For step ≥ 10, marks become noisy. Options:
- Show all marks (default for ≤ 10 steps)
- Show only labeled marks (e.g., every 5th)
- No marks at all (use ranges with labels above)
Document chosen variant in component instance.
R6 — Numeric label format (per locale)
Per i18n/contract.kmd:
- Decimal separator:
1.5(en-US) /1,5(pt-BR) - Thousands separator:
1,000/1.000 - Currency / units: locale-aware
R7 — Keyboard navigation
| Key | Action (single) | Action (range) |
|---|---|---|
| Tab | Focus into handle | Focus first handle; Tab again moves to second |
| Arrow Left / Down | Decrement by step | Decrement focused handle |
| Arrow Right / Up | Increment by step | Increment focused handle |
| Page Down | Decrement by 10× step | Same |
| Page Up | Increment by 10× step | Same |
| Home | Jump to minimum | Min handle to absolute min OR max handle to current min |
| End | Jump to maximum | Symmetric |
R8 — Accessibility
- Container:
role="group"+aria-label(range slider needs group because it has two handles) - Single handle:
role="slider"+aria-valuenow/aria-valuemin/aria-valuemax+aria-label - Range handles: each handle has
role="slider"with own valuenow / min / max; min handle's max = current max handle value (and vice versa) aria-valuetextfor formatted value (e.g., "75 percent")- Live region announces value change during drag (polite)
- Don't rely on color alone for active vs inactive segments
R9 — Animation
- Drag: 1:1 with input (no easing)
- Release-to-step (discrete): snap with motion-emphasized-decelerate (~200 ms)
- Value label appear: scale-in 0.95 → 1 + fade (motion-fast)
- Value label disappear: fade-out (motion-fast)
- Handle scale-up on hover / focus: motion-fast
- Reduced motion: no scale animation, no spring; instant changes
R10 — Range thumb collision
When two handles meet (min approaches max):
- Default: handles cannot pass each other; second handle blocks
- Optional: handles can swap (min handle's value becomes greater than initial max → roles reassign)
Pick policy per instance and document. Default is "no pass".
R11 — Density
| Density | Track | Handle |
|---|---|---|
| Compact | 2 px | 16 px |
| Default | 4 px | 20 px |
| Comfortable | 6 px | 24 px |
Hit target stays 48 × 48 px regardless.
R12 — Per-preset variation
| Preset | Track | Handle | Label |
|---|---|---|---|
material3 | Pill, 4 px gap each side of handle | 20 px circle | Pill-chip above |
material2 | Plain bar | Solid 18 px circle | No label by default |
ios_cupertino | Thin track | Larger 28 px handle with shadow | Inline above |
gnome | GtkScale narrow track | Small 14 px handle | None |
windows_11 | Accent track | Pill-shaped handle | Tooltip above |
brutalist | Solid block | Square handle, 2 px border | Black-on-white pill |
terminal_classic | [████░░░░░░] 40 ASCII | n/a | Numeric trailing |
R13 — Forbidden patterns
- ❌ Slider for values with NO meaningful intermediate (use radio / segmented buttons)
- ❌ Slider without value indication (label or visible value somewhere)
- ❌ Hit target < 48 × 48 px around handle
- ❌ Range slider where one handle can hide behind the other
- ❌ Drag that updates value only on release (laggy / unresponsive)
- ❌ Step intervals so dense they look continuous (use a continuous slider with snap-on-release instead)
- ❌ Slider for >100 steps (use input field / numeric stepper)
- ❌ Color alone for active / inactive (must work in monochrome)
- ❌ Slider that triggers irreversible action on drag (e.g., file delete, sale checkout) — use confirmation pattern
Cross-link
themes/color-roles.kmd—primaryfor active track + handlethemes/motion.kmd— emphasized-decelerate for snapthemes/typography.kmd—label-mediumfor valuei18n/contract.kmd— locale-aware numeric formatinteraction/states.kmd— hover / focused / dragging layersfoundations/elements.kmd— Control family
Requirements (testable)
Requirement: Discrete slider snaps to the nearest step {#req-sliders-snap-to-step}
A discrete slider SHALL constrain its committed value to the defined step positions: on release the handle settles to the nearest step, and no intermediate off-step value is ever committed.
Scenario: Drag between two steps releases to nearest
- GIVEN a discrete slider with steps at 1, 2, 3, 4, 5 and the handle at 2
- WHEN the user drags the handle to a pixel position between step 3 and step 4, closer to step 3, and releases
- THEN the handle animates to step 3
- AND the committed value is 3
Scenario: Keyboard increment moves exactly one step
- GIVEN a focused discrete slider with step interval 1 and current value 3
- WHEN the user presses Arrow Right
- THEN the value becomes 4
- AND the value is a valid step position
Requirement: Range handles do not cross under the default no-pass policy {#req-sliders-range-no-pass}
A range slider under the default collision policy SHALL prevent either handle from passing the other: the min handle's value can never exceed the max handle's value and the max handle's value can never fall below the min handle's value, honoring any configured minimum gap.
Scenario: Min handle blocked at max handle
- GIVEN a range slider from 0 to 100 with min handle at 35 and max handle at 60
- WHEN the user drags the min handle rightward past the max handle position
- THEN the min handle stops at 60 (or at the configured minimum-gap distance below 60)
- AND the min handle value never exceeds the max handle value
Scenario: Both handles remain independently addressable
- GIVEN a range slider with both handles at the same nominal value after a collision attempt
- WHEN the user inspects the two handles
- THEN neither handle is fully hidden behind the other
- AND each handle can still receive focus and be dragged apart
Requirement: Value label is visible during drag and keyboard focus {#req-sliders-value-label}
The slider SHALL surface the current value: the value label is always shown while a drag is in progress and while a handle is focused via keyboard, so the user always has value indication during interaction.
Scenario: Label appears on drag
- GIVEN a slider at rest with the value label hidden
- WHEN the user begins dragging the handle
- THEN the value label becomes visible above the handle
- AND the label text reflects the handle's current value
Scenario: Label appears on keyboard focus
- GIVEN a slider whose handle is not focused
- WHEN the user Tabs focus onto the handle
- THEN the value label is shown
- AND it updates as Arrow keys change the value
Requirement: Keyboard navigation adjusts value by step and to bounds {#req-sliders-keyboard}
A focused slider handle SHALL be operable entirely from the keyboard: Arrow keys change the value by one step, Page Up/Down by ten steps, and Home/End jump to the minimum and maximum.
Scenario: Page keys move by ten steps
- GIVEN a focused single slider with step 1, minimum 0, maximum 100, current value 40
- WHEN the user presses Page Up
- THEN the value becomes 50
- AND it does not exceed the maximum of 100
Scenario: Home and End jump to bounds
- GIVEN a focused single slider with minimum 0 and maximum 100 at value 40
- WHEN the user presses Home
- THEN the value becomes 0
- WHEN the user then presses End
- THEN the value becomes 100
Requirement: Slider exposes ARIA slider semantics {#req-sliders-aria}
Each slider handle SHALL expose role="slider" with aria-valuenow, aria-valuemin, and aria-valuemax; a range slider SHALL wrap its two handles in a role="group" container and each handle's advertised bound SHALL be clamped by the other handle's current value.
Scenario: Single handle advertises current and bounds
- GIVEN a single continuous slider with minimum 0, maximum 100, value 75
- WHEN an assistive technology queries the handle
- THEN the handle has role="slider"
- AND aria-valuenow is 75, aria-valuemin is 0, and aria-valuemax is 100
Scenario: Range handles clamp each other's advertised bounds
- GIVEN a range slider with min handle at 35 and max handle at 120 inside a role="group" container
- WHEN an assistive technology queries each handle
- THEN the min handle's aria-valuemax equals 120 (the current max handle value)
- AND the max handle's aria-valuemin equals 35 (the current min handle value)
Requirement: Disabled slider blocks all interaction {#req-sliders-disabled}
A disabled slider SHALL NOT respond to pointer or keyboard input and SHALL render the entire control at 38% opacity, leaving its value unchanged by any interaction attempt.
Scenario: Drag on a disabled slider does nothing
- GIVEN a disabled slider rendered at 38% opacity with value 50
- WHEN the user attempts to drag the handle
- THEN the handle does not move
- AND the value remains 50
References
specs/foundations/elements.kmdspecs/themes/color-roles.kmdspecs/themes/motion.kmdspecs/themes/typography.kmdspecs/interaction/states.kmd