Skip to content

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

All triggers

Specification body

Spec — Sliders

Facet Visual of Koder Design. Material parity: https://m3.material.io/components/sliders.

Variant matrix

ContinuousDiscrete
SingleSmooth drag, any valueDrag snaps to steps
RangeTwo handles, smoothTwo 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): primary color
  • Handle: 20 px circle, primary color, 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-primary color (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 visibleDetail
Drag in progressAlways
Handle focused (keyboard)Always
Handle hovered (mouse)Optional (configurable)
IdleHidden (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
  • primary bg, on-primary text
  • label-medium (12/16, weight 500)
  • Anchored 12 px above handle; tip pointing down

R4 — States

StateVisual
RestDefault
Hovered (handle / track)Subtle scale of handle to 24 px
Focused4 px focus ring outside handle
DraggingHandle scale to 24 px + value label visible
Disabled38% 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

KeyAction (single)Action (range)
TabFocus into handleFocus first handle; Tab again moves to second
Arrow Left / DownDecrement by stepDecrement focused handle
Arrow Right / UpIncrement by stepIncrement focused handle
Page DownDecrement by 10× stepSame
Page UpIncrement by 10× stepSame
HomeJump to minimumMin handle to absolute min OR max handle to current min
EndJump to maximumSymmetric

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-valuetext for 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

DensityTrackHandle
Compact2 px16 px
Default4 px20 px
Comfortable6 px24 px

Hit target stays 48 × 48 px regardless.

R12 — Per-preset variation

PresetTrackHandleLabel
material3Pill, 4 px gap each side of handle20 px circlePill-chip above
material2Plain barSolid 18 px circleNo label by default
ios_cupertinoThin trackLarger 28 px handle with shadowInline above
gnomeGtkScale narrow trackSmall 14 px handleNone
windows_11Accent trackPill-shaped handleTooltip above
brutalistSolid blockSquare handle, 2 px borderBlack-on-white pill
terminal_classic[████░░░░░░] 40 ASCIIn/aNumeric 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
  • themes/color-roles.kmdprimary for active track + handle
  • themes/motion.kmd — emphasized-decelerate for snap
  • themes/typography.kmdlabel-medium for value
  • i18n/contract.kmd — locale-aware numeric format
  • interaction/states.kmd — hover / focused / dragging layers
  • foundations/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