Skip to content
Components, switch product

Slider

<gui-slider> picks a number on a scale. The user drags the thumb, presses the track, or moves it with the keyboard. The thumb is a native <input type="range">, with a label, hint and errors, and the value shows at the end of the label’s line.

Code of the example above
SliderBasic.tsx
import { GuiSlider } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function SliderBasic() {
return (
<GuiSlider
label="Volume"
value={40}
/>
);
}

To pick a low and a high value, such as a price range, use the Range slider. For a number the user types, use the Number field.

The element’s entry point is @golemui/gui-components/slider, which registers <gui-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.

VolumeField.tsx
import { useState } from 'react';
import { GuiSlider } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function VolumeField() {
const [volume, setVolume] = useState(50);
return (
<GuiSlider
label="Volume"
name="volume"
value={volume}
onGuiChange={(event) => setVolume(event.detail.value ?? 50)}
/>
);
}

The value is a number, or undefined until the user moves the thumb. The snippets start at 50. gui-input fires at each step while the user drags, and gui-change once, when they let go.

Properties

Property Type Default Description
value number – The number, or undefined before the user picks one.
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 the thumb. detail.value is the new value.
gui-change The user let go of the thumb, or moved it with a key. detail.value is the value.
gui-blur Focus left the control.

value is a number. As an attribute, value="40" sets it; from JavaScript, set the value property.

Until the user moves the thumb, or value is set, the slider has no value. The thumb rests at minimum with a neutral border and nothing filled, the value reads ”–”, and screen readers hear “Not set”. The thumb at minimum is not a value: the slider in In a form starts this way. To pick minimum, the user presses Home or a key toward it, or presses the thumb where it rests.

minimum and maximum are the ends of the scale, 0 and 100 by default. step is the distance between two values, counted from minimum, and 1 by default. Steps can be decimals.

Code of the example above
SliderBoundsStep.tsx
import { GuiSlider } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function SliderBoundsStep() {
return (
<div className="gui-grid gui-grid--row">
<div className="gui-grid__cell">
<GuiSlider
label="Guests"
minimum={1}
maximum={12}
value={2}
/>
</div>
<div className="gui-grid__cell">
<GuiSlider
label="Rating"
maximum={5}
step={0.5}
value={3.5}
/>
</div>
</div>
);
}

marks with no value draws a tick at every step, up to 101 ticks. A slider with more steps draws none. A JSON list draws ticks only at its values: each item is a number, or an object with a value and a label that shows under the track. Ticks up to the thumb take the fill color.

Code of the example above
SliderMarks.tsx
import { GuiSlider } from '@golemui/gui-components/react';
import type { GuiSliderMark } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const speeds: GuiSliderMark[] = [
{ value: 0.5, label: 'Slow' },
{ value: 1, label: 'Normal' },
1.5,
{ value: 2, label: 'Fast' },
];
export function SliderMarks() {
return (
<div className="gui-grid">
<div className="gui-grid__cell">
<GuiSlider
label="Brightness"
step={10}
value={60}
marks
/>
</div>
<div className="gui-grid__cell">
<GuiSlider
label="Playback speed"
minimum={0.5}
maximum={2}
step={0.25}
value={1}
marks={speeds}
/>
</div>
</div>
);
}

The labels show as you write them: format doesn’t change them. Marks outside the bounds are left out.

format takes the options of Intl.NumberFormat: as an attribute, a JSON object; from JavaScript, an object. {"style":"percent"} shows 0.8 as 80%, and a currency style shows an amount of money. locale-id sets the separators and the symbol’s place, en by default.

Code of the example above
SliderFormat.tsx
import { GuiSlider } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
const percent: Intl.NumberFormatOptions = { style: 'percent' };
const euros: Intl.NumberFormatOptions = {
style: 'currency',
currency: 'EUR',
maximumFractionDigits: 0,
};
export function SliderFormat() {
return (
<div className="gui-grid">
<div className="gui-grid__cell">
<GuiSlider
label="Opacity"
maximum={1}
step={0.05}
value={0.8}
format={percent}
/>
</div>
<div className="gui-grid__cell">
<GuiSlider
label="Monthly budget"
maximum={5000}
step={100}
value={1500}
format={euros}
localeId="de"
/>
</div>
</div>
);
}

The format applies to the value next to the label, to the bubble and to what screen readers read. The value itself stays a plain number, such as 0.8 or 1500.

value-placement sets where the value shows:

  • label, the default: at the end of the label’s line.
  • bubble: over the thumb, while the thumb is hovered, focused or dragged.
  • both: in both places.
  • hidden: nowhere, for an app that shows the value its own way.

Screen readers read the value from the thumb in every case. Hover or focus the sliders below.

Code of the example above
SliderValuePlacement.tsx
import { GuiSlider } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function SliderValuePlacement() {
return (
<div className="gui-grid">
<div className="gui-grid__cell">
<GuiSlider
label="Next to the label"
value={25}
/>
</div>
<div className="gui-grid__cell">
<GuiSlider
label="In a bubble"
value={50}
valuePlacement="bubble"
/>
</div>
<div className="gui-grid__cell">
<GuiSlider
label="Both"
value={75}
valuePlacement="both"
/>
</div>
<div className="gui-grid__cell">
<GuiSlider
label="Hidden"
value={100}
valuePlacement="hidden"
/>
</div>
</div>
);
}

Escape hides the bubble until the pointer and the focus leave the slider. See Accessibility.

gui-input fires at each step while the user drags, so the app can preview the value as it changes. gui-change fires once, when the user lets go: save the value there. A key press fires both. event.detail.value is the value in each one.

Preview: 100%. Saved: 100%.

Code of the example above
SliderEvents.tsx
import { useState } from 'react';
import { GuiSlider } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function SliderEvents() {
const [preview, setPreview] = useState(100);
const [saved, setSaved] = useState(100);
return (
<>
<GuiSlider
id="zoom-slider"
label="Zoom"
minimum={50}
maximum={200}
step={10}
value={preview}
onGuiInput={(event) => setPreview(event.detail.value ?? 100)}
onGuiChange={(event) => setSaved(event.detail.value ?? 100)}
/>
<p id="zoom-status" role="status">
Preview: {preview}%. Saved: {saved}%.
</p>
</>
);
}

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

Code of the example above
SliderDisabledReadonly.tsx
import { GuiSlider } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function SliderDisabledReadonly() {
return (
<div className="gui-grid gui-grid--row">
<div className="gui-grid__cell">
<GuiSlider
label="Bass"
value={30}
disabled
/>
</div>
<div className="gui-grid__cell">
<GuiSlider
label="Treble"
value={70}
readOnly
/>
</div>
</div>
);
}

hint and errors work as on the text input.

With a name, <gui-slider> submits its number as text, such as 7 or 0.5, under that name. A slider without a value submits nothing. These block a submit:

  • required, while the slider has no value. 0 is a value.
  • A value set from outside the bounds, such as value="150" with maximum="100". The slider keeps the value and shows it next to the label, the thumb stops at the end, and the browser reports “Value must be less than or equal to 100.” or “greater than or equal to …”.
  • A value set off a step, such as value="42" with step="5". The slider keeps the value and shows it next to the label, the thumb sits at the nearest step, and the browser reports “Choose a valid value. The nearest is 40.”

The user can’t go past the bounds or off a step: the thumb stops at the ends, and on the steps. Submit the form below without moving the slider, then pick a score and submit again.

Code of the example above
SliderNativeForm.tsx
import { useState, type SubmitEvent } from 'react';
import { GuiButton, GuiSlider } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function SliderNativeForm() {
const [result, setResult] = useState('');
const onSubmit = (event: SubmitEvent<HTMLFormElement>) => {
event.preventDefault();
const data = Object.fromEntries(new FormData(event.currentTarget));
setResult(`Submitted ${JSON.stringify(data)}`);
};
return (
<form id="survey" onSubmit={onSubmit}>
<div className="gui-grid">
<div className="gui-grid__cell">
<GuiSlider
label="How likely are you to recommend us?"
name="score"
maximum={10}
marks
required
/>
</div>
<div className="gui-grid__cell">
<GuiButton type="submit" label="Send" />
</div>
</div>
<p id="survey-result" role="status">
{result}
</p>
</form>
);
}

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

<gui-slider> renders into the light DOM, so you can style every part with plain CSS. These are the parts and their public classes, with a hint, marks with labels, value-placement="both" and an error:

gui-slider
<gui-slider class="gui-field">
<label class="gui-label" for="volume" id="volume_label">
<span class="gui-label__text">
Volume<span aria-hidden="true"> *</span>
</span>
<div class="gui-widget-hint" id="volume_hint" aria-hidden="true">
Applies to every speaker.
</div>
<output class="gui-slider__value" aria-hidden="true">40</output>
</label>
<div class="gui-widget">
<div class="gui-slider__control">
<div class="gui-slider__rail" aria-hidden="true">
<span class="gui-slider__fill" style="--_start: 0; --_end: 40;"></span>
</div>
<div class="gui-slider__ticks" aria-hidden="true">
<span
class="gui-slider__tick gui-slider__tick--filled"
style="--_percent: 0;"
></span>
<span class="gui-slider__tick" style="--_percent: 50;"></span>
<span class="gui-slider__tick" style="--_percent: 100;"></span>
</div>
<input
type="range"
class="gui-slider__input"
id="volume"
min="0"
max="100"
step="10"
aria-valuetext="40"
aria-describedby="volume_hint"
aria-required="true"
aria-invalid="true"
aria-errormessage="volume_errors"
/>
<span
class="gui-slider__bubble"
aria-hidden="true"
style="--_percent: 40;"
>
40
</span>
</div>
</div>
<div class="gui-slider__footer">
<div class="gui-slider__tick-labels" aria-hidden="true">
<span
class="gui-slider__tick-label gui-slider__tick-label--first"
style="--_percent: 0;"
>
Mute
</span>
<span
class="gui-slider__tick-label gui-slider__tick-label--last"
style="--_percent: 100;"
>
Max
</span>
</div>
<div class="gui-validator" id="volume_errors" role="alert">
<div class="gui-validator__error">Turn it up a little.</div>
</div>
</div>
</gui-slider>
ClassPart
gui-fieldThe element itself. Lines its parts up with the fields next to it in a gui-grid row.
gui-label, gui-label__textThe label, and its text with the required marker.
gui-widget-hintThe hint.
gui-slider__valueThe value at the end of the label’s line. Screen readers skip it: they read the thumb.
gui-widgetThe box around the control.
gui-slider__controlThe rail, the ticks, the thumb and the bubble. It is as tall as a text field, so the slider lines up with its row.
gui-slider__control--emptyOn the control while there is no value: the thumb takes a neutral border.
gui-slider__control--dismissedOn the control after Escape hid the bubble.
gui-slider__railThe track.
gui-slider__fillThe filled part of the track, from minimum to the thumb.
gui-slider__ticks, gui-slider__tickThe tick marks, and one tick.
gui-slider__tick--filledOn a tick in the filled part.
gui-slider__inputThe native range input. Its own track is transparent; the thumb is its ::-webkit-slider-thumb and ::-moz-range-thumb.
gui-slider__bubbleThe value over the thumb, with its arrow drawn as its ::before.
gui-slider__footerWhat goes under the control: the tick labels, then the errors.
gui-slider__tick-labelsThe labels of the marks.
gui-slider__tick-labelOne label. --first and --last line the labels at the ends up with the edges of the slider.
gui-validator, gui-validator__errorThe errors container, always there so the alert role announces new errors, and one error message.

The element places the fill, the ticks, the labels and the bubble with private custom properties in their inline styles: --_start, --_end and --_percent. Don’t set them yourself. Server HTML carries them as style attributes: see Strict CSP. The ids derive from uid, or from a generated id such as gui-slider-3 when you don’t set one. Select by class or by tag, not by id.

The slider reads these design tokens. Set them on :root to change every slider, or on any element to change the sliders inside it:

TokenWhat it styles
--gui-slider-thumb-sizeThe width and height of the thumb, 1.25rem by default.
--gui-slider-track-sizeThe thickness of the track, 0.25rem by default. The ticks are twice as large.
--gui-widget-sizeThe height of the control, as for a text field.
--gui-border-strongThe track, the ticks, and the thumb’s border while there is no value.
--gui-intent-primaryThe filled part, the filled ticks and the thumb’s border.
--gui-bg-defaultThe thumb.
--gui-shadow-smThe thumb’s shadow.
--gui-shadow-focusThe ring around the thumb while it has keyboard focus or is dragged.
--gui-transition-normalHow fast the ring shows, 0.2s when unset.
--gui-tooltip-bgThe bubble’s background. Unset, it takes --gui-bg-inverse.
--gui-tooltip-textThe bubble’s text. Unset, it takes --gui-text-inverse.
--gui-radius-smThe bubble’s corners.
--gui-text-defaultThe label and the value.
--gui-font-familyThe font.
--gui-font-smThe text size of the label and the value.
--gui-font-xsThe text size of the hint, the bubble, the tick labels and the errors.
--gui-text-secondaryThe hint and the tick labels.
--gui-intent-errorThe thumb’s border and the messages when there are errors.
--gui-bg-disabledThe track, the ticks and the thumb when disabled.
--gui-text-disabledThe filled part and the thumb’s border when disabled.
--gui-space-1, --gui-space-2The space above the tick labels, the bubble’s padding and the gap before the value.
styles.css
/* A larger thumb on a thicker track. */
gui-slider {
--gui-slider-thumb-size: 1.5rem;
--gui-slider-track-size: 0.375rem;
}

--gui-tooltip-bg and --gui-tooltip-text also color tooltips. To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.

  • Role. The thumb is a native <input type="range">, so screen readers announce a slider with its minimum, maximum and value.
  • Name. The label names the slider. Without a label the slider has no accessible name.
  • Value. The thumb reads its value through aria-valuetext, in the slider’s format and locale-id, such as “80%” or “1.500 €”. Before there is a value, it reads “Not set”: translate it under the key notSet. The value next to the label, the bubble, the ticks and their labels are hidden from screen readers.
  • Description and errors. The hint is the slider’s description. While errors show, the thumb has aria-invalid="true" and points at them with aria-errormessage, and screen readers announce new errors as they appear.
  • Required and read-only. required sets aria-required and readonly sets aria-readonly. A read-only slider stays in the tab order.
  • Bubble. The bubble shows while the thumb is hovered or focused, and stays while the pointer is on it. Escape hides it, and does nothing else: in a dialog, the first Escape hides the bubble and the next one closes the dialog. The bubble shows again once the pointer and the focus have left the slider.
KeyAction
TabMoves focus to the thumb, and away from it.
→ ↑Moves one step up.
← ↓Moves one step down.
Page Up Page DownMoves a larger step up or down, which the browser sets: a tenth of the scale in Chrome.
Home EndMoves to minimum or maximum.
EscapeHides the bubble, without closing a dialog around the slider.

Each key that moves the thumb fires gui-input and gui-change, and so does a key that picks minimum on a slider with no value.

What every element shares, such as translating the browser messages, is in Accessibility.