Skip to content

Selection

Slider

Lets people pick a value or a range from a track: five sizes, stops, centered and vertical sliders and an inset icon.

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.

React HTML
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.

React HTML
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.

React HTML
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: input while the value changes and change when a drag, press or key ends (Compose's onValueChangeFinished), both CustomEvents with detail: { 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. __track is 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.