Skip to content
Components, switch product

Range slider

<gui-range-slider> picks a range on a scale: a low and a high value, such as a price range. It is a gui-slider with two thumbs, one for each end. The thumbs can meet but not cross.

Code of the example above
RangeSliderBasic.tsx
import { GuiRangeSlider } from '@golemui/gui-components/react';
import type { GuiSliderRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const price: GuiSliderRange = { start: 100, end: 300 };
const dollars: Intl.NumberFormatOptions = {
style: 'currency',
currency: 'USD',
maximumFractionDigits: 0,
};
export function RangeSliderBasic() {
return (
<GuiRangeSlider
label="Price"
maximum={500}
step={10}
value={price}
format={dollars}
/>
);
}

The element’s entry point is @golemui/gui-components/range-slider, which registers <gui-range-slider> and nothing else. To install the package and load the stylesheet, see Installation.

The @golemui/gui-components/react components have typed props and an on* prop for each event. React 18 or later.

PriceField.tsx
import { useState } from 'react';
import { GuiRangeSlider } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function PriceField() {
const [price, setPrice] = useState({ start: 0, end: 500 });
return (
<GuiRangeSlider
label="Price"
name="price"
maximum={500}
value={price}
onGuiChange={(event) => setPrice(event.detail.value ?? { start: 0, end: 500 })}
/>
);
}

The value is an object with a start and an end, or undefined until the user moves a thumb. The snippets start with the whole scale. As on the slider, gui-input fires while the user drags and gui-change when they let go.

Properties

Property Type Default Description
value GuiSliderRange – The range, or undefined before the user picks one.
startLabel attribute start-label string – The name of the low thumb, after the label. Defaults to "Minimum".
endLabel attribute end-label string – The name of the high thumb, after the label. Defaults to "Maximum".
localeId attribute locale-id string 'en' BCP 47 locale for formatting the value, such as en-US or es.
minimum number 0 The lowest value. Defaults to 0.
maximum number 100 The highest value. Defaults to 100.
step number 1 The step between values, from minimum. Defaults to 1.
marks boolean | GuiSliderMark[] – Tick marks on the track: true for one at every step (up to 101), or a list of values, or of { value, label } with a label under the tick.
format Intl.NumberFormatOptions – How the value reads: Intl.NumberFormat options, such as { style: 'currency', currency: 'EUR' } or { style: 'percent' }. Used next to the label, in the bubbles and for screen readers. Mark labels show as they are written.
valuePlacement attribute value-placement GuiSliderValuePlacement 'label' Where the value shows: label, at the end of the label's line (the default); bubble, over the thumb while it is hovered, focused or dragged; both; or hidden, for an app that shows it its own way. Screen readers read it from the thumb in every case.
uid string – The id of the element's control. The ids of its label, hint and errors derive from it. Optional: when it is not set, the element generates one from its tag name and a page-wide counter, e.g. gui-textinput-3. Generated ids depend on creation order, so a server-rendered element must set uid or its ids will not match after hydration.

Form control properties

Every field has these, from the base class of the form controls.

Property Type Default Description
name string – The name the value is submitted under. Without it the element stays out of its form. Reflected, like a native control's: the form reads a single value's name from the attribute.
label string – The visible label, which also names the control for assistive technology. Without one the control has no accessible name; the date, time, range, tags and file fields warn about it in development builds.
hint string – Help text shown under the label and announced as the control's description.
errors string[] [] Error messages to show, typically from your own validation. They show as soon as they are set, unless touched is false.
touched boolean – Whether the user has interacted with the control. Leave it unset unless you validate on interaction: false holds errors back until it becomes true.
required boolean false The control needs a value: it is marked as required and, with a name, blocks submission.
readOnly attribute readonly boolean – The value can be read and focused but not changed.
disabled boolean – Disabled by its own attribute or by a disabled <fieldset> around it. Setting the property sets the attribute, like a native control's.
form read-only, no attribute HTMLFormElement | null – The <form> the element belongs to, if any.
validity read-only, no attribute ValidityState – The element's validity, like a native control's.
validationMessage read-only, no attribute string – The message the browser shows for an invalid value.

Methods

Method Description
checkValidity(): boolean –
reportValidity(): boolean –

Events

Every event bubbles and is composed, so you can listen on any ancestor.

Event Description
gui-input The user moved a thumb. detail.value is the new { start, end }.
gui-change The user let go of a thumb, or moved it with a key. detail.value is the range.
gui-blur Focus left the range slider: not when it moves from one thumb to the other.

Everything on the Slider page applies too: minimum, maximum and step, marks, format and locale-id, value-placement, and the disabled and read-only states.

value is an object, { start, end }. As an attribute, it is JSON, such as value='{"start":100,"end":300}'. Next to the label, the value reads as both ends, such as “$100 – $300”.

Until the user moves a thumb, or value is set, the range slider has no value: the thumbs rest at the ends, nothing is filled, the value reads ”–”, and screen readers hear “Not set” on both thumbs. The first move sets both ends: the thumb the user moves, and the other one where it rests. A key or a press that leaves a thumb at its end, such as Home on the start thumb, picks the whole range, from minimum to maximum.

A thumb stops when it meets the other one, so start is never above end. A press on the track, away from the thumbs, moves the nearer thumb there and focuses it. With value-placement="bubble", each thumb has its own bubble.

Code of the example above
RangeSliderMarksBubble.tsx
import { GuiRangeSlider } from '@golemui/gui-components/react';
import type { GuiSliderMark, GuiSliderRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const ages: GuiSliderRange = { start: 25, end: 40 };
const decades: GuiSliderMark[] = [
{ value: 18, label: '18' },
{ value: 30, label: '30' },
{ value: 42, label: '42' },
{ value: 54, label: '54' },
{ value: 66, label: '66' },
{ value: 78, label: '78' },
];
export function RangeSliderMarksBubble() {
return (
<GuiRangeSlider
label="Age"
minimum={18}
maximum={78}
value={ages}
marks={decades}
valuePlacement="bubble"
/>
);
}

Screen readers name each thumb after the label: “Age Minimum” and “Age Maximum”. start-label and end-label replace “Minimum” and “Maximum”, for a scale where other words fit better.

Code of the example above
RangeSliderThumbNames.tsx
import { GuiRangeSlider } from '@golemui/gui-components/react';
import type { GuiSliderMark, GuiSliderRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const departure: GuiSliderRange = { start: 8, end: 14 };
const hours: GuiSliderMark[] = [
{ value: 0, label: '00:00' },
{ value: 12, label: '12:00' },
{ value: 24, label: '24:00' },
];
export function RangeSliderThumbNames() {
return (
<GuiRangeSlider
label="Departure time"
maximum={24}
value={departure}
startLabel="Earliest"
endLabel="Latest"
marks={hours}
/>
);
}

To translate the default names for every range slider, use the keys minimum and maximum. See i18n.

event.detail.value is the whole range, { start, end }, in gui-input and gui-change. With value-placement="hidden", the app can show the range its own way, as below. Save it on gui-change.

Open from 9:00 to 18:00.

Code of the example above
RangeSliderEvents.tsx
import { useState } from 'react';
import { GuiRangeSlider } from '@golemui/gui-components/react';
import type { GuiSliderRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
export function RangeSliderEvents() {
const [hours, setHours] = useState<GuiSliderRange>({ start: 9, end: 18 });
return (
<>
<GuiRangeSlider
id="hours-slider"
label="Opening hours"
minimum={6}
maximum={24}
value={hours}
valuePlacement="hidden"
onGuiInput={(event) => setHours(event.detail.value ?? hours)}
/>
<p id="hours-status" role="status">
Open from {hours.start}:00 to {hours.end}:00.
</p>
</>
);
}

gui-blur fires when focus leaves the range slider: moving from one thumb to the other doesn’t count.

A disabled range slider can’t be focused or moved, and a form doesn’t submit it. A readonly range slider can be focused, but the keys, the pointer and a press on the track don’t move it, and a form submits its value.

Code of the example above
RangeSliderDisabledReadonly.tsx
import { GuiRangeSlider } from '@golemui/gui-components/react';
import type { GuiSliderRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const quiet: GuiSliderRange = { start: 22, end: 24 };
const work: GuiSliderRange = { start: 9, end: 17 };
export function RangeSliderDisabledReadonly() {
return (
<div className="gui-grid gui-grid--row">
<div className="gui-grid__cell">
<GuiRangeSlider
label="Quiet hours"
maximum={24}
value={quiet}
disabled
/>
</div>
<div className="gui-grid__cell">
<GuiRangeSlider
label="Work hours"
maximum={24}
value={work}
readOnly
/>
</div>
</div>
);
}

With a name, <gui-range-slider> submits the range as JSON under that name, such as {"start":3,"end":7}. Without a value it submits nothing. required blocks the submit until the user moves a thumb, and a start or an end set from outside the bounds or off a step blocks it as on the slider. Submit the form below without moving the thumbs, then pick a range and submit again.

Code of the example above
RangeSliderNativeForm.tsx
import { useState, type SubmitEvent } from 'react';
import { GuiButton, GuiRangeSlider } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function RangeSliderNativeForm() {
const [result, setResult] = useState('');
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => {
event.preventDefault();
const nights = new FormData(event.currentTarget).get('nights');
setResult(`Submitted ${nights}`);
};
return (
<form id="search" onSubmit={onSubmit}>
<div className="gui-grid">
<div className="gui-grid__cell">
<GuiRangeSlider
label="Nights"
name="nights"
minimum={1}
maximum={14}
marks
required
/>
</div>
<div className="gui-grid__cell">
<GuiButton type="submit" label="Search" />
</div>
</div>
<p id="search-result" role="status">
{result}
</p>
</form>
);
}

How the elements submit, validate and reset is in Native forms.

<gui-range-slider> renders into the light DOM, so you can style every part with plain CSS. The label is a <span>, since it names two thumbs. Each thumb is followed by its bubble and by its name, which only screen readers read. This one has value-placement="both":

gui-range-slider
<gui-range-slider class="gui-field">
<span class="gui-label" id="price_label">
<span class="gui-label__text">Price</span>
<output class="gui-slider__value" aria-hidden="true">$100 – $300</output>
</span>
<div class="gui-widget">
<div class="gui-slider__control gui-slider__control--range">
<div class="gui-slider__rail" aria-hidden="true">
<span class="gui-slider__fill" style="--_start: 20; --_end: 60;"></span>
</div>
<input
type="range"
class="gui-slider__input"
id="price_start"
min="0"
max="500"
step="10"
aria-labelledby="price_label price_start_name"
aria-valuetext="$100"
/>
<span
class="gui-slider__bubble"
aria-hidden="true"
style="--_percent: 20;"
>
$100
</span>
<span class="gui-visually-hidden" id="price_start_name">Minimum</span>
<input
type="range"
class="gui-slider__input"
id="price_end"
min="0"
max="500"
step="10"
aria-labelledby="price_label price_end_name"
aria-valuetext="$300"
/>
<span
class="gui-slider__bubble"
aria-hidden="true"
style="--_percent: 60;"
>
$300
</span>
<span class="gui-visually-hidden" id="price_end_name">Maximum</span>
</div>
</div>
<div class="gui-slider__footer">
<div
class="gui-validator gui-validator--empty"
id="price_errors"
role="alert"
></div>
</div>
</gui-range-slider>

It has the classes of the Slider, with these differences:

ClassPart
gui-slider__control--rangeOn the control. It takes the presses on the track; each input takes the pointer on its thumb only.
gui-slider__fillThe filled part, between the two thumbs.
gui-slider__tick--filledOn a tick between the two thumbs.
gui-visually-hiddenThe name of a thumb, such as “Minimum”, read by screen readers only.

The ids derive from uid, with _start and _end for the thumbs, or from a generated id such as gui-range-slider-3 when you don’t set one. Select by class or by tag, not by id.

The range slider reads the Slider’s tokens. Set --gui-slider-thumb-size and --gui-slider-track-size on gui-range-slider, or on any element around it:

styles.css
/* Smaller thumbs on a thin track. */
gui-range-slider {
--gui-slider-thumb-size: 1rem;
--gui-slider-track-size: 0.125rem;
}

To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.

Everything on the Slider applies to each thumb, with these differences:

  • Names. Each thumb is a slider named by the label and its own name, “Minimum” or “Maximum”: “Price Minimum” and “Price Maximum”. Change the names with start-label and end-label, or translate them under the keys minimum and maximum.
  • Value. Each thumb reads its own end through aria-valuetext, such as “$100”, or “Not set” before there is a range.
  • Description, errors and states. The hint, the errors, required and readonly apply to both thumbs: each one has the description, aria-invalid, aria-required and aria-readonly.
  • Focus. Both thumbs are tab stops, the low one first. gui-blur fires only when focus leaves both.
KeyAction
TabMoves focus to the low thumb, then to the high one, and away.
← → ↓ ↑Move the focused thumb one step. It stops at the other thumb.
Page Up Page DownMove the focused thumb a larger step, up to the other thumb.
Home EndMove the focused thumb as far as it goes: to the end of the scale, or to the other thumb.
EscapeHides the bubbles, without closing a dialog around the range slider.

What every element shares, such as translating the default names, is in Accessibility.