sveltelean Primitives
Versionv0.2.0 GitHub

Example

Volume and temperature
<label for="slider-volume">Volume</label>
<input id="slider-volume" type="range" name="volume" min="0" max="100" step="5" value="40"
	data-slean="slider" />

<label for="slider-temperature">Temperature</label>
<input id="slider-temperature" type="range" name="temperature" min="16" max="28" step="0.5"
	value="21" data-slean="slider" />
Tier 0: drag the thumb or use the arrow keys, PageUp, PageDown, Home and End. In Firefox the track fills up to the thumb; in Chrome and Safari it does not (see below). Everything here works with page JavaScript disabled.

Why this implementation exists

<input type="range"> is a complete slider: pointer and touch dragging, the arrow, Page, Home and End keys, min, max and step, the value in the form, and the slider role with its value, range and orientation exposed to assistive technology. A slider component rebuilds each of these with pointer events and ARIA.

What the platform leaves open is the look. The stylesheet draws the track and the thumb through the engines’ pseudo-elements, and here the gap is honest: only Firefox styles the filled part of the track (::-moz-range-progress). Chrome and Safari have no such pseudo-element, and filling the track there needs script that reads the value, so the styled slider shows the thumb on an unfilled track. data-variant="native" keeps the browser’s own drawing instead, which Chrome and Firefox draw with a filled track.

The browser owns

  • pointer dragging, min, max and step
  • the arrow, Page, Home and End keys
  • the slider role with its value, range and orientation
  • the form value and the input and change events
  • the filled track in Firefox, and on its own drawing the filled track and datalist tick marks

Svelte Lean owns

  • slider.css: the track and thumb from the tokens, the hover halo, the native variant, the vertical option
  • the contract
  • documentation

Usage

The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.

npm install @svelte-lean/styles
stylesheets
import '@svelte-lean/styles/slider.css';

Put data-slean="slider" on an <input type="range"> with a label, min, max, step and value. Bind the value with bind:value; it is a number.

Show the exact number next to the slider with an <output for> when it matters. Add aria-valuetext when the number alone does not describe the value.

data-variant="native" for the platform’s drawing with the filled track and tick marks; data-slean-orientation="vertical" for a vertical slider. For two thumbs, use Range slider.

Anatomy

PartElementdata-slean-partRequiredNotes
root<input type="range" data-slean="slider">–yesA label; min, max, step, value. data-variant="native" keeps the platform drawing; data-slean-orientation="vertical".
marks<datalist id="…"><option value="…"></option></datalist>–noNamed by the list attribute; tick marks on the native variant only.

Runtime profile

Tier 0: the slider has no behavior module, the Vite plugin maps slider to no module, and the native-only fixture proves the path ships no Svelte Lean JavaScript.

Accessibility contract

  • The platform exposes the slider role, the value, the minimum, the maximum and the orientation from the input itself; no ARIA attribute is needed for them.
  • The arrow keys change the value by step, PageUp and PageDown by a larger amount, Home and End go to the bounds; under dir="rtl" ArrowLeft and ArrowRight follow the visual direction.
  • aria-valuetext replaces the number when the number is not the value’s meaning (a side, a size, a level).
  • The focus ring surrounds the input on keyboard focus. Under a coarse pointer the input and the thumb grow to the touch target size.
  • Forced colors: the track takes the text color and the thumb the Highlight system color.

Keyboard

KeyWhenResult
ArrowRight/ArrowUpslider focusedOne step up; ArrowRight is one step down under RTL (native)
ArrowLeft/ArrowDownslider focusedOne step down; ArrowLeft is one step up under RTL (native)
PageUp/PageDownslider focusedA tenth of the range up or down (native, Chrome)
Home/Endslider focusedThe minimum, the maximum (native)

Platform features

FeatureBaselineOutside the target
<input type="range">Widely availableNot applicable
Filled trackFirefox only (::-moz-range-progress)The thumb on an unfilled track; data-variant="native" for the platform’s drawing
accent-colorNot Baseline: Chrome 93, Firefox 92, Safari 26.2The native variant in the platform’s color
Vertical form controls (writing-mode)Baseline 2024 (Chrome 124, Firefox 120, Safari 17.4)The vertical slider is drawn horizontally
Datalist tick marksChrome, Firefox 109, Safari 12.1, on the native drawing onlyNo tick marks

Without JavaScript

Fully functional.

Server rendering

The value, min, max and step attributes render the initial state; the thumb is in place before any script runs.

Before hydration

Nothing is attached; the slider works before and after hydration alike.

Styling

slider.css resets the input’s appearance and draws the track and the thumb with ::-webkit-slider-runnable-track and ::-webkit-slider-thumb (Chrome, Safari) and ::-moz-range-track, ::-moz-range-thumb and ::-moz-range-progress (Firefox), with a halo on hover. The native variant restores the platform’s appearance and sets accent-color. The tokens it reads:

TokenDefault (light)Applies to
--slean-control-height-sm2remblock-size
--slean-radius-sm0.375remborder-radius
--slean-accentoklch(54% 0.19 258)accent-color, background
--slean-radius-full9999pxborder-radius
--slean-bordervar(--slean-neutral-6)background
--slean-surfaceoklch(100% 0 0)border
--slean-shadow-sm0 1px 2px oklch(0% 0 0 / 0.08)box-shadow
--slean-duration-fast100mstransition
--slean-easecubic-bezier(0.2, 0, 0, 1)transition
--slean-accent-softoklch(95% 0.03 258)box-shadow
--slean-control-height-md2.25remblock-size

State selectors the stylesheet targets, all from the platform or ARIA: ::-moz-range-progress, ::-moz-range-thumb, ::-moz-range-track, ::-webkit-slider-runnable-track, ::-webkit-slider-thumb, :disabled, :hover.

Variant attributes: data-variant (native); data-slean-orientation (vertical).

Compatibility notes

The range input is widely available. The filled track is drawn by Firefox only; the native variant takes the accent color where accent-color is supported (Chrome 93, Firefox 92, Safari 26.2; not Baseline). Vertical sliders through writing-mode are Baseline 2024. Tick marks appear only on the platform’s drawing.

Examples

The platform’s drawing and tick marks

data-variant="native" keeps the browser’s own slider and colors it with accent-color. Chrome and Firefox draw it with the track filled up to the thumb, and the browsers draw a tick mark for each option of the <datalist> named by list. The styled track has neither in Chrome and Safari.

native drawing
<label for="slider-zoom">Zoom</label>
<input id="slider-zoom" type="range" name="zoom" min="50" max="150" step="25" value="100"
	list="slider-zoom-marks" data-slean="slider" data-variant="native" />
<datalist id="slider-zoom-marks">
	<option value="50"></option>
	<option value="75"></option>
	<option value="100"></option>
	<option value="125"></option>
	<option value="150"></option>
</datalist>

Vertical

data-slean-orientation="vertical" sets writing-mode: vertical-lr and direction: rtl. The browser then lays the slider out vertically with the minimum at the bottom, keeps ArrowUp as one step up, and reports the vertical orientation to assistive technology. The same two declarations work in an application’s own CSS.

vertical
<input type="range" name="low" value="60" aria-label="Low"
	data-slean="slider" data-slean-orientation="vertical" />
<input type="range" name="mid" value="35" aria-label="Middle"
	data-slean="slider" data-slean-orientation="vertical" />
<input type="range" name="high" value="75" aria-label="High"
	data-slean="slider" data-slean-orientation="vertical" />
the same, in CSS
/* The vertical option is these two declarations; they work without the attribute. */
input[type='range'].vertical {
	writing-mode: vertical-lr;
	direction: rtl; /* the minimum at the bottom */
}

Value text

When the number does not describe the value, aria-valuetext does; screen readers read it instead of the number. It changes with the value, so the application keeps it up to date.

balance.svelte
<script lang="ts">
	let balance = $state(0);
	// The number alone does not say which side: the text is the application's job.
	const text = $derived(
		balance === 0 ? 'Centered' : `${Math.abs(balance)} to the ${balance < 0 ? 'left' : 'right'}`
	);
</script>

<label for="balance">Balance</label>
<input id="balance" type="range" min="-10" max="10" bind:value={balance}
	aria-valuetext={text} data-slean="slider" />

Testing

Source