Skip to main content

Range Slider

Two-thumb range for min/max. Built on overlapping native range inputs.

Quick start
import { RangeSlider } from "mates-ui";

RangeSlider({ value: [20, 80], onChange: () => {} });

Basic range

import { html, atom, renderApp } from 'mates';
import { RangeSlider, Text } from 'mates-ui';

const App = () => {
const range = atom([20, 75]);
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:35rem;width:100%;">
${RangeSlider({ value: range(), onChange: (v) => range.set(v), label: "Value range" })}
${Text(range()[0] + " – " + range()[1], { type: "body-sm", color: "muted" })}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

Formatted value labels

import { html, atom, renderApp } from 'mates';
import { RangeSlider } from 'mates-ui';

const App = () => {
const pct = atom([10, 90]);
const price = atom([150, 800]);
return () => html`
<x-col gap="var(--md-space-6)" style="max-width:35rem;width:100%;">
${RangeSlider({ value: pct(), onChange: (v) => pct.set(v), label: "Percentage", showTips: true, format: (v) => v + "%" })}
${RangeSlider({ value: price(), onChange: (v) => price.set(v), label: "Price", min: 0, max: 1000, step: 10, showTips: true, format: (v) => "$" + v })}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

Step and ticks

import { html, atom, renderApp } from 'mates';
import { RangeSlider } from 'mates-ui';

const App = () => {
const range = atom([20, 60]);
return () => html`
<x-col gap="var(--md-space-4)" style="max-width:35rem;width:100%;">
${RangeSlider({ value: range(), onChange: (v) => range.set(v), label: "Coarse range (step 10)", step: 10, showTips: true })}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

Disabled

import { html, renderApp } from 'mates';
import { RangeSlider } from 'mates-ui';

const App = () => () => html`
<div style="max-width:35rem;width:100%;">
${RangeSlider({ value: [30, 70], disabled: true, label: "Disabled range", showTips: true })}
</div>
`;

renderApp(App, document.getElementById('app'));

Focus trap between handles

import { html, atom, renderApp } from 'mates';
import { RangeSlider, Text } from 'mates-ui';

const App = () => {
const range = atom([15, 85]);
return () => html`
<x-col gap="var(--md-space-4)" style="max-width:35rem;width:100%;">
${Text("Tab / Shift+Tab cycle only the two handles. Escape blurs.", { type: "body-sm", color: "muted" })}
${RangeSlider({ value: range(), onChange: (v) => range.set(v), label: "Trap between handles", trapFocus: true })}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

Single Slider (for reference)

import { html, atom, renderApp } from 'mates';
import { Slider, Text } from 'mates-ui';

const App = () => {
const val = atom(45);
return () => html`
<x-col gap="var(--md-space-3)" style="max-width:35rem;width:100%;">
${Slider({ value: val(), showTip: true, label: "Single value", onInput: (v) => val.set(v) })}
${Text("Value: " + val(), { type: "body-sm", color: "muted" })}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

Track height

import { html, atom, renderApp } from 'mates';
import { RangeSlider, Slider } from 'mates-ui';

const App = () => {
const range = atom([25, 75]);
return () => html`
<x-col gap="var(--md-space-5)" style="max-width:35rem;width:100%;">
${RangeSlider({ value: range(), onChange: (v) => range.set(v), label: "Default — 0.25rem" })}
${RangeSlider({ value: range(), onChange: (v) => range.set(v), label: "0.375rem", trackHeight: "0.375rem", showTips: true })}
${RangeSlider({ value: range(), onChange: (v) => range.set(v), label: "0.5rem + marks", trackHeight: "0.5rem", showTips: true, marks: true, step: 10 })}
${RangeSlider({ value: range(), onChange: (v) => range.set(v), label: "0.75rem", trackHeight: "0.75rem", showTips: true })}
${Slider({ value: range()[0], trackHeight: "0.5rem", showTip: true, marks: true, step: 10, label: "Single — 0.5rem", onInput: (v) => range.set([v, range()[1]]) })}
</x-col>
`;
};

renderApp(App, document.getElementById('app'));

RangeSlider

import { RangeSlider } from "mates-ui";
Prop Type Default Description
attr AttrMap Extra HTML attributes applied to the root element.
classes string | string[] Extra root classes — joined and appended after built-in classes.
data DataAttrMap Root `data-*` map (`testId` → `data-testid`). `testId` prop / `attr` win over `data`.
disabled boolean false Disables both thumbs and prevents any interaction.
format (v: number) => string Formats the displayed value labels. Applied to both the min and max thumb.
id string Root `id` shorthand (`attr.id` wins). Form native-control `id` props are unchanged.
label string Accessible label for the slider group.
max number 100 Upper bound of the slider track.
min number 0 Lower bound of the slider track.
onChange ([min, max]: [number, number]) => void Called with the new [min, max] tuple whenever either thumb moves.
showTips boolean false When true, shows a balloon-style tip above each thumb with the current value.
step number 1 Granularity of each thumb movement.
ticks boolean false Shows tick marks along the track at each step interval.
trapFocus boolean false When true, Tab / Shift+Tab cycle only between the two handles; Escape blurs.
value* [number, number] Controlled [min, max] tuple representing the current range.

CSS tokens

Token Role
--md-color-border Unfilled track background colour.
--md-color-primary Track fill colour and thumb accent colour.
--md-color-surface-raised Thumb background colour.

Caveats

  • `value` must always be `[min, max]` where `value[0] <= value[1]`.
  • The two thumbs cannot cross — the library clamps them automatically.
  • `format` applies to both the min and max value labels.