Selection
Slider
Lets people pick a value or a range from a track: five sizes, stops, centered and vertical sliders and an inset icon.
- COMPONENT_SPEC §25
- CSS · custom elements · React
- Open in the playground
Examples
Standard slider
Continuous with an icon in the track, discrete with stops every 10, and disabled. Each thumb is a native range input: the arrow keys, Page Up / Down, Home and End work.
The live demo runs with JavaScript. Its code is below.
import { Slider } from '@kyralabs/m3exp-web/react';
export default function Standard() {
return (
<div style={{ display: 'grid', gap: 'var(--m3e-sys-space-300)', inlineSize: '100%' }}>
<Slider aria-label="Volume" defaultValue={0.4} icon="volume_up" size="m" />
<Slider aria-label="Brightness" defaultValue={60} minValue={0} maxValue={100} step={10} />
<Slider aria-label="Disabled" defaultValue={0.3} isDisabled />
</div>
);
}
<div style="display: grid; gap: var(--m3e-sys-space-300); inline-size: 100%">
<m3e-slider label="Volume" value="0.4" icon="volume_up" size="m"></m3e-slider>
<m3e-slider label="Brightness" min="0" max="100" step="10" value="60"></m3e-slider>
<m3e-slider label="Disabled" value="0.3" disabled></m3e-slider>
</div>
Run the HTML version (custom elements, no framework).
Range slider
Two thumbs that meet but never cross, each with its own label. The values format as currency.
The live demo runs with JavaScript. Its code is below.
import { RangeSlider } from '@kyralabs/m3exp-web/react';
export default function Range() {
return (
<RangeSlider
aria-label="Price"
thumbLabels={['Minimum price', 'Maximum price']}
minValue={0}
maxValue={500}
step={10}
defaultValue={[100, 350]}
formatOptions={{ style: 'currency', currency: 'EUR', maximumFractionDigits: 0 }}
/>
);
}
<m3e-slider
range
label="Price"
start-label="Minimum price"
end-label="Maximum price"
min="0"
max="500"
step="10"
start-value="100"
end-value="350"
></m3e-slider>
Run the HTML version (custom elements, no framework).
Centered and vertical
A centered slider fills from its middle; a vertical one sizes to its block size.
The live demo runs with JavaScript. Its code is below.
import { Slider } from '@kyralabs/m3exp-web/react';
export default function CenteredVertical() {
return (
<div style={{ display: 'grid', gap: 'var(--m3e-sys-space-300)', inlineSize: '100%' }}>
<Slider
aria-label="Balance"
variant="centered"
minValue={-50}
maxValue={50}
defaultValue={20}
/>
<Slider
aria-label="Bass"
orientation="vertical"
size="l"
defaultValue={0.6}
style={{ blockSize: 'calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-900))' }}
/>
</div>
);
}
<div style="display: grid; gap: var(--m3e-sys-space-300); inline-size: 100%">
<m3e-slider label="Balance" variant="centered" min="-50" max="50" value="20"></m3e-slider>
<m3e-slider
label="Bass"
orientation="vertical"
size="l"
value="0.6"
style="block-size: calc(var(--m3e-sys-space-900) + var(--m3e-sys-space-900))"
></m3e-slider>
</div>
Run the HTML version (custom elements, no framework).
API
React
<Slider>
Slider (COMPONENT_SPEC §25, Compose Slider / VerticalSlider): React Aria useSlider / useSliderThumb on the m3e-css .m3e-slider contract (the same DOM as <m3e-slider>), with Compose's value rules from m3e-motion: stops for a step, Compose's key steps and pages. The ref is the root. variant="centered" is SliderDefaults.CenteredTrack; orientation="vertical" puts the minimum at the bottom; icon adds an inset icon (M / L / XL).
| Prop | Type | Default | Description |
|---|---|---|---|
value
|
number
|
Controlled value. | |
variant
|
'standard' | 'centered'
|
centered: the active track runs from the middle (SliderDefaults.CenteredTrack).
|
|
icon
|
string
|
Inset Material Symbols icon (standard M / L / XL sliders; m3.material.io). | |
minValue
|
number
|
Minimum value (default 0, Compose's trackRange). | |
maxValue
|
number
|
Maximum value (default 1). | |
step
|
number
|
The value increment. With a step the slider has stops (ticks) and snaps to them; without (or 0) it is continuous and a key moves 1 % of the range (Compose). | |
size
|
"xs" | "s" | "m" | "l" | "xl"
|
XS (default), S, M, L, XL (m3.material.io token DB sizes). | |
orientation
|
'horizontal' | 'vertical'
|
Vertical: the minimum at the bottom; give the slider a block-size. | |
formatOptions
|
Intl.NumberFormatOptions
|
Number formatting of the value text (default: at most two fraction digits). | |
name
|
string
|
Form field name (each thumb's native input). | |
defaultValue
|
number
|
||
isDisabled
|
boolean
|
||
id
|
string
|
||
className
|
string
|
||
style
|
CSSProperties
|
||
aria-label
|
string
|
||
aria-labelledby
|
string
|
||
aria-describedby
|
string
|
||
onChange
|
(value: number) => void
|
Called while the value changes (Compose onValueChange). | |
onChangeEnd
|
(value: number) => void
|
Called when a drag, press or key ends (Compose onValueChangeFinished). |
<RangeSlider>
Range slider (COMPONENT_SPEC §25, Compose RangeSlider): two thumbs, each its own labelled slider ("Range start" / "Range end", Compose's descriptions), that meet but never cross. Horizontal (m3.material.io: avoid vertical range sliders).
| Prop | Type | Default | Description |
|---|---|---|---|
value
|
[number, number]
|
Controlled start and end. | |
thumbLabels
|
[string, string]
|
The thumbs' labels (default: Compose's "Range start" / "Range end"). | |
minValue
|
number
|
Minimum value (default 0, Compose's trackRange). | |
maxValue
|
number
|
Maximum value (default 1). | |
step
|
number
|
The value increment. With a step the slider has stops (ticks) and snaps to them; without (or 0) it is continuous and a key moves 1 % of the range (Compose). | |
size
|
"xs" | "s" | "m" | "l" | "xl"
|
XS (default), S, M, L, XL (m3.material.io token DB sizes). | |
orientation
|
'horizontal' | 'vertical'
|
Vertical: the minimum at the bottom; give the slider a block-size. | |
formatOptions
|
Intl.NumberFormatOptions
|
Number formatting of the value text (default: at most two fraction digits). | |
name
|
string
|
Form field name (each thumb's native input). | |
defaultValue
|
[number, number]
|
||
isDisabled
|
boolean
|
||
id
|
string
|
||
className
|
string
|
||
style
|
CSSProperties
|
||
aria-label
|
string
|
||
aria-labelledby
|
string
|
||
aria-describedby
|
string
|
||
onChange
|
(value: [number, number]) => void
|
||
onChangeEnd
|
(value: [number, number]) => void
|
Custom elements
<m3e-slider>
import '@kyralabs/m3e-elements/slider.js';
<m3e-slider label="Volume" value="0.4"> (COMPONENT_SPEC §25): renders the m3e-css slider inside itself (the host is display: contents) and adds what CSS cannot do:
- Pointer: a press on the track moves the thumb there (a range slider: the closer thumb, Compose's RangeSliderLogic) and drags it; values snap to the stops; range thumbs meet but never cross.
- Keyboard, on each thumb's native
<input type="range">(its role, value and form name): arrows by one step (step, or 1 % of the range), Shift + arrow and PageUp / PageDown by a page, Home / End to the thumb's bounds; horizontal arrows mirror in RTL, and a vertical slider has its minimum at the bottom (owner decision M8 #21). Shared with m3e-react through m3e-motion's slider math and the slider-layout entry. [data-dragging]on the dragged thumb (its handle halves and the value indicator shows; keyboard focus does the same through:focus-visible).- Events:
inputwhile the value changes andchangewhen a drag, press or key ends (Compose's onValueChangeFinished), bothCustomEvents withdetail: { value }or, for a range,{ start, end }.
Attributes: min (0), max (1), step (absent or 0: continuous), value; range with start-value / end-value (the thumbs are labelled start-label / end-label, default "Range start" / "Range end" as Compose); variant (standard | centered), orientation (horizontal | vertical; set a vertical slider's block-size), the deprecated booleans centered and vertical (since 1.8, removed in 2.0; variant / orientation win), size (xs, s, m, l, xl), icon (an inset Material Symbols icon, standard M / L / XL), disabled, label (the accessible name), name.
| Attribute | Property | Type | Notes |
|---|---|---|---|
min
|
min
|
number
|
|
max
|
max
|
number
|
|
step
|
step
|
number
|
|
value
|
value
|
number
|
|
range
|
range
|
boolean
|
|
start-value
|
startValue
|
number
|
|
end-value
|
endValue
|
number
|
|
variant
|
variant
|
'standard' | 'centered'
|
standard (default) | centered (React's variant).
|
orientation
|
orientation
|
'horizontal' | 'vertical'
|
horizontal (default) | vertical (set its block-size).
|
centered
|
centered
|
boolean
|
Deprecated.
Since 1.8: use variant="centered". Removed in 2.0. A centered slider.
|
vertical
|
vertical
|
boolean
|
Deprecated.
Since 1.8: use orientation="vertical". Removed in 2.0. A vertical slider.
|
disabled
|
disabled
|
boolean
|
|
size
|
size
|
string
|
|
label
|
label
|
string
|
|
start-label
|
startLabel
|
string
|
|
end-label
|
endLabel
|
string
|
|
icon
|
icon
|
string
|
|
name
|
name
|
string
|
| Event | Detail | Description |
|---|---|---|
input
|
SliderValueDetail
|
While the value changes (the inputs' native input stops at the slider).
|
change
|
SliderValueDetail
|
When a drag, press or key ends (Compose onValueChangeFinished). |
CSS
slider.css
@import '@kyralabs/m3e-css/slider.css';
Slider (COMPONENT_SPEC §25): standard, range (two thumbs), centered and vertical, sizes XS–XL, stops (ticks), an inset icon (M–XL) and the value indicator. This file draws a static slider from three custom properties on the root; <m3e-slider> / m3e-react's <Slider> and <RangeSlider> write them and add pointer, keyboard and value events.
<div class="m3e-slider" role="group" aria-label="Volume"
style="--m3e-comp-slider-value: 0.4; --m3e-comp-slider-steps: 4">
<div class="m3e-slider__track">
<span class="m3e-slider__inactive-start" aria-hidden="true"></span>
<span class="m3e-slider__active" aria-hidden="true"></span>
<span class="m3e-slider__inactive-end" aria-hidden="true"></span>
<span class="m3e-slider__ticks" aria-hidden="true">
<span class="m3e-slider__tick" style="--m3e-comp-slider-tick-index: 0"></span>
… one per stop, 0 … steps + 1 …
</span>
<div class="m3e-slider__thumb">
<input class="m3e-slider__input" type="range" min="0" max="1" step="0.2" value="0.4"
aria-valuetext="0.4" aria-orientation="horizontal" aria-labelledby="…" />
<span class="m3e-slider__handle" aria-hidden="true"></span>
<span class="m3e-slider__value-indicator" aria-hidden="true">0.4</span>
</div>
</div>
</div>
Inputs: --m3e-comp-slider-value (0–1, the thumb or a range's end), --m3e-comp-slider-start-value (0–1, a range's start), --m3e-comp-slider-steps (Compose steps: stops between the ends; 0 = continuous). Modifiers: --range (two thumbs, the first .m3e-slider__thumb--start), --centered (SliderDefaults.CenteredTrack), --vertical (minimum at the bottom; give the slider a block-size), --size-xs (default), --size-s, --size-m, --size-l, --size-xl. Inset icon (standard M / L / XL only): two copies of the icon in the track, .m3e-icon.m3e-slider__icon (shown on the active track when it fits) and .m3e-icon.m3e-slider__icon.m3e-slider__icon--inactive (shown at the start of the inactive track otherwise). The track parts, ticks and inset icon are optional for a standard slider without stops; JS layers render them all.
61 custom properties it reads
| Custom property | Default |
|---|---|
--m3e-comp-divider-thickness
|
1px |
--m3e-comp-minimum-interactive-component-size
|
48px |
--m3e-comp-slider-handle-shape
|
var(--m3e-sys-shape-corner-full) |
--m3e-comp-slider-icon-color-active
|
var(--m3e-sys-color-secondary-container) |
--m3e-comp-slider-icon-color-active-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.12 * 100%), transparent) |
--m3e-comp-slider-icon-color-inactive
|
var(--m3e-sys-color-primary) |
--m3e-comp-slider-icon-color-inactive-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-slider-large-icon-padding
|
6px |
--m3e-comp-slider-large-icon-size
|
24px |
--m3e-comp-slider-large-thumb-height
|
68px |
--m3e-comp-slider-large-track-corner-size
|
16px |
--m3e-comp-slider-large-track-height
|
56px |
--m3e-comp-slider-medium-icon-padding
|
6px |
--m3e-comp-slider-medium-icon-size
|
24px |
--m3e-comp-slider-medium-thumb-height
|
44px |
--m3e-comp-slider-medium-track-corner-size
|
12px |
--m3e-comp-slider-medium-track-height
|
40px |
--m3e-comp-slider-small-thumb-height
|
44px |
--m3e-comp-slider-small-track-corner-size
|
8px |
--m3e-comp-slider-small-track-height
|
24px |
--m3e-comp-slider-start-value
|
set by the component |
--m3e-comp-slider-steps
|
set by the component |
--m3e-comp-slider-stop-indicator-shape
|
var(--m3e-sys-shape-corner-full) |
--m3e-comp-slider-thumb-color
|
var(--m3e-sys-color-primary) |
--m3e-comp-slider-thumb-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), var(--m3e-sys-color-surface)) |
--m3e-comp-slider-thumb-track-gap-size
|
6px |
--m3e-comp-slider-thumb-width
|
4px |
--m3e-comp-slider-tick-color-active
|
var(--m3e-sys-color-secondary-container) |
--m3e-comp-slider-tick-color-active-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.12 * 100%), transparent) |
--m3e-comp-slider-tick-color-inactive
|
var(--m3e-sys-color-primary) |
--m3e-comp-slider-tick-color-inactive-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-slider-tick-index
|
set by the component |
--m3e-comp-slider-tick-size
|
4px |
--m3e-comp-slider-track-color-active
|
var(--m3e-sys-color-primary) |
--m3e-comp-slider-track-color-active-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-slider-track-color-inactive
|
var(--m3e-sys-color-secondary-container) |
--m3e-comp-slider-track-color-inactive-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.12 * 100%), transparent) |
--m3e-comp-slider-track-inside-corner-size
|
2px |
--m3e-comp-slider-track-stop-indicator-color
|
var(--m3e-sys-color-primary) |
--m3e-comp-slider-track-stop-indicator-color-disabled
|
color-mix(in srgb, var(--m3e-sys-color-on-surface) calc(0.38 * 100%), transparent) |
--m3e-comp-slider-track-stop-indicator-size
|
4px |
--m3e-comp-slider-value
|
set by the component |
--m3e-comp-slider-value-indicator-active-bottom-space
|
12px |
--m3e-comp-slider-value-indicator-container-color
|
var(--m3e-sys-color-inverse-surface) |
--m3e-comp-slider-value-indicator-container-height
|
44px |
--m3e-comp-slider-value-indicator-container-shape
|
var(--m3e-sys-shape-corner-full) |
--m3e-comp-slider-value-indicator-container-width
|
48px |
--m3e-comp-slider-value-indicator-label-text-color
|
var(--m3e-sys-color-inverse-on-surface) |
--m3e-comp-slider-value-indicator-label-text-font
|
var(--m3e-sys-typescale-label-large-font) |
--m3e-comp-slider-value-indicator-label-text-line-height
|
var(--m3e-sys-typescale-label-large-line-height) |
--m3e-comp-slider-value-indicator-label-text-size
|
var(--m3e-sys-typescale-label-large-size) |
--m3e-comp-slider-value-indicator-label-text-tracking
|
var(--m3e-sys-typescale-label-large-tracking) |
--m3e-comp-slider-value-indicator-label-text-weight
|
var(--m3e-sys-typescale-label-large-weight) |
--m3e-comp-slider-x-large-icon-padding
|
8px |
--m3e-comp-slider-x-large-icon-size
|
32px |
--m3e-comp-slider-x-large-thumb-height
|
108px |
--m3e-comp-slider-x-large-track-corner-size
|
28px |
--m3e-comp-slider-x-large-track-height
|
96px |
--m3e-comp-slider-x-small-thumb-height
|
44px |
--m3e-comp-slider-x-small-track-corner-size
|
8px |
--m3e-comp-slider-x-small-track-height
|
16px |
Accessibility
one native <input type="range"> per thumb (its implicit slider role, value, aria-valuetext (at most two fraction digits, Compose's formatForSemantics), aria-orientation, form name, and increments from assistive technology), invisible in the thumb, labelled by the slider's role="group"; range thumbs are each their own slider labelled "Range start" / "Range end" (Compose's descriptions). Keys (Compose slideOnKeyEvents): arrows by one step (range / (steps + 1), or 1 % of the range without steps), PageUp / PageDown by clamp(actualSteps / 10, 1, 10) steps, Home / End to the thumb's bounds; horizontal arrows mirror in RTL; vertical up increases; the web adds the other arrow pair (ARIA) and Shift + arrow for a page (m3.material.io). Focus: the project ring around the handle; it does not widen the gaps (owner decision M8 #23).
Every interactive part has a target of at least 48 × 48 px, a visible focus ring for
keyboard focus only, and works under forced-colors and
prefers-reduced-motion.
Sources
Values come from androidx Compose Material3 1.5.0-beta01 at 19991f2c and from
m3.material.io, as cited below from COMPONENT_SPEC §25 and the m3e-css sources.
SliderTokens.kt@19991f2c (the XS set) plus the Slider.kt / SliderDefaults values Compose renders and the m3.material.io sizes (components/slider-defaults.css; SOURCES register #42). Track (TrackHeight = InactiveTrackHeight) 16dp, the slider less the thumb width, placed thumb width / 2 in, ≥ 48dp across (minimum interactive size); thumb ThumbWidth × ThumbHeight 4 × 44dp (vertical: swapped); each track part ends ThumbWidth / 2 + ThumbTrackGapSize (ActiveHandleLeadingSpace, 6dp) from a thumb centre, 2dp inside corners (TrackInsideCornerSize), outer corners the corner size (default Track: thickness / 2); with steps a thumb off the end stops sits at (track − 2 × corner) × fraction + corner; parts shorter than the corner size (range / centered active part: the inside corner) are not drawn unless corners shrink (Track(trackCornerSize) and CenteredTrack, without steps). Stop indicators (TrackStopIndicatorSize 4dp) one corner size in from the end of the inactive part (range / centered: both ends); ticks (TickSize 4dp) at lerp(corner, length − corner, i / (steps + 1)), skipped in a thumb gap (centered: also the centre gap) and where a stop indicator is. Colors (defaultSliderColors): active track and handle primary, inactive track secondary-container, active ticks secondary-container, inactive ticks and the end stop primary (owner decision M8 #20); disabled tracks and ticks on-surface at 0.38 / 0.12 (translucent), the disabled handle on-surface 0.38 composited over surface. Hover changes no color (owner decision M8 #3); no ripple or state layer (Compose has none). Sizes (owner decision M8 #16): the m3.material.io token DB (TOKEN_TABLE.0045edb815f2325e, md.comp.slider.{xsmall…xlarge}): track 16 / 24 / 40 / 56 / 96dp, handle 44 / 44 / 44 / 68 / 108dp (M: the DB's 44dp, not the Specs table's 52dp; owner decision M8 #16, D19 (8), register #68), corner 8 / 8 / 12 / 16 / 28dp; S–XL shrink corners like Compose's Track(trackCornerSize); stop indicators one corner size in, as Compose draws them (Slider.kt L2550–2566; not the DB's 4dp trailing space: D19 (9), register #70). Inset icon (owner decision M8 #18): standard M / L / XL only, 24 / 24 / 32dp with 6 / 6 / 8dp padding (DB), on the active part when icon + 2 × padding fits (SliderWithTrackIconsSample), else at the start of the inactive part (m3.material.io), in the tick color of its part; no stop indicator with an icon. Value indicator (owner decision M8 #19): SliderTokens.ValueIndicator* (inverse-surface, inverse-on-surface label large, 12dp above the handle), 48 × 44dp (m3.material.io Measurements), fully rounded (the Specs redline; register #69), shown while dragged or keyboard focused; a vertical slider's at the inline start of the thumb.
slider.css sources
(compose/material3 @ 19991f2c, Slider.kt; values in slider.css / slider-defaults.css, lines in PARITY.md):
- Layout (SliderImpl L878–1082): minimumInteractiveComponentSize (≥ 48dp across); the track is the slider less the thumb width, placed thumb width / 2 in; a thumb is centred on its value: track × fraction, or with steps (off the first and last stop) (track − 2 × corner) × fraction + corner.
__trackis that box (the pointer target, the full 48dp across) and a size container: 100cqi (vertical: 100cqb) is the track length. - Track (SliderDefaults.drawTrack L2398–2671): parts end ThumbWidth / 2 + ThumbTrackGapSize (2 + 6dp) from a thumb centre, with 2dp inside corners towards the thumb and the track corner size outside (default Track: thickness / 2; S–XL: the token DB corner size). A part is drawn only when longer than a threshold: the corner size (the inside corner for range / centered active parts), or 0 when corners shrink (Track(trackCornerSize) and CenteredTrack, without steps). CSS scales over-large radii like Skia's RRect, so short parts shrink their corners. Centered (CenteredTrack): the active part runs between the centre and the thumb; the gap on the centre side of the thumb has no thumb width while the thumb is past the centre. Booleans come from round(up, x, step): 1 (or the track length) for any x > 0, 0 for x = 0.
- Stop indicators (drawStopIndicator): 4dp dots one corner size in from the end of the inactive part (a range or centered slider: both ends), in the active track color (owner decision M8 #20), drawn with their part. None with an inset icon (SliderWithTrackIconsSample).
- Ticks: dots at lerp(corner, length − corner, i / (steps + 1)); the stop indicators replace the last (range / centered: and the first); dots within a thumb gap (or a centered slider's centre gap) are not drawn; active ticks (on the active part) the inactive track color, others the active track color (owner decision M8 #20).
- Thumb (ThumbContent L2727–2773): ThumbWidth × ThumbHeight (vertical: swapped), its handle's width (height) halved while pressed or dragged ([data-dragging]) or keyboard focused, at once: no animation (owner decision M8 #17). The hand cursor over the thumb. No ripple or state layer (Compose's slider has none). Focus: the project ring around the handle; it does not widen the gaps (owner decision M8 #23).
- Colors (defaultSliderColors L1716–1757): disabled parts translucent, the disabled handle composited over surface. Hover changes nothing (owner decision M8 #3).
- Sizes (owner decision M8 #16): the m3.material.io token DB (slider-defaults.css): track thickness 16 / 24 / 40 / 56 / 96dp, handle 44 / 44 / 44 / 68 / 108dp (M: the DB, not the Specs table's 52dp; D19 (8), SOURCES register #68), corner 8 / 8 / 12 / 16 / 28dp; S–XL shrink corners like the Compose samples' Track(trackCornerSize). Stop indicators sit one corner size in, as Compose draws them (Slider.kt L2550–2566; the token DB's 4dp trailing space is not used: D19 (9), register #70).
- Inset icon (owner decision M8 #18): M / L / XL 24 / 24 / 32dp, padding 6 / 6 / 8dp; on the active part when icon + 2 × padding fits (SliderWithTrackIconsSample), else at the start of the inactive part (m3.material.io "the icon moves to the inactive track"); colors: the tick colors of its part.
- Value indicator (owner decision M8 #19): SliderTokens.ValueIndicator* (inverse surface, label large, 12dp above the handle), 48 × 44dp (m3.material.io Measurements), fully rounded (the Specs redline; register #69), while dragging or keyboard focused; a vertical slider's shows at the inline start of the thumb.