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.
import { GuiSlider } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function SliderBasic() { return ( <GuiSlider label="Volume" value={40} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/slider';
@Component({ selector: 'app-slider-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-slider label="Volume" value="40" ></gui-slider> `,})export class SliderBasicComponent {}<script setup lang="ts">import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';</script>
<template> <gui-slider label="Volume" :value="40" ></gui-slider></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
@customElement('slider-basic')export class SliderBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-slider label="Volume" value="40" ></gui-slider> `; }}<gui-slider label="Volume" value="40"></gui-slider>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.
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)} /> );}Add CUSTOM_ELEMENTS_SCHEMA to the component that uses the tags, and the stylesheet to styles in angular.json.
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { GuiEventMap, GuiSliderEvents } from '@golemui/gui-components';import '@golemui/gui-components/slider';
type ChangeEvent = GuiEventMap<typeof GuiSliderEvents>['gui-change'];
@Component({ selector: 'app-volume-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-slider label="Volume" name="volume" [value]="volume" (gui-change)="onChange($event)" ></gui-slider> `,})export class VolumeFieldComponent { volume = 50;
onChange(event: Event) { this.volume = (event as ChangeEvent).detail.value ?? 50; }}"styles": ["node_modules/@golemui/gui-components/lib/styles/index.css"]Tell Vue's compiler that gui-* tags are custom elements, and add the typings once for typed props and @gui-* handlers.
<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
const volume = ref(50);</script>
<template> <gui-slider label="Volume" name="volume" :value="volume" @gui-change="volume = $event.detail.value ?? 50" /></template>vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('gui-') } },});import type {} from '@golemui/gui-components/vue';The page's stylesheet styles the gui-* elements, so it only reaches them outside a shadow root. Render the host component into the light DOM too, as below, or load the stylesheet into its shadow root.
import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
@customElement('volume-field')export class VolumeField extends LitElement { @state() private volume = 50;
protected override createRenderRoot() { return this; }
override render() { return html` <gui-slider label="Volume" name="volume" .value=${this.volume} @gui-change=${(event: CustomEvent) => (this.volume = event.detail.value)} ></gui-slider> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-slider label="Volume" name="volume"></gui-slider>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-slider');
element.addEventListener('gui-change', (event) => { console.log(event.detail.value);});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. |
Examples
Section titled “Examples”The value
Section titled “The value”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.
Bounds and step
Section titled “Bounds and step”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/slider';
@Component({ selector: 'app-slider-bounds-step', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-slider label="Guests" minimum="1" maximum="12" value="2" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Rating" maximum="5" step="0.5" value="3.5" ></gui-slider> </div> </div> `,})export class SliderBoundsStepComponent {}<script setup lang="ts">import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-slider label="Guests" :minimum="1" :maximum="12" :value="2" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Rating" :maximum="5" :step="0.5" :value="3.5" ></gui-slider> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
@customElement('slider-bounds-step')export class SliderBoundsStep extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-slider label="Guests" minimum="1" maximum="12" value="2" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Rating" maximum="5" step="0.5" value="3.5" ></gui-slider> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-slider label="Guests" minimum="1" maximum="12" value="2" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Rating" maximum="5" step="0.5" value="3.5" ></gui-slider> </div></div>Tick marks
Section titled “Tick marks”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { GuiSliderMark } from '@golemui/gui-components';import '@golemui/gui-components/slider';
@Component({ selector: 'app-slider-marks', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Brightness" step="10" value="60" marks ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Playback speed" minimum="0.5" maximum="2" step="0.25" value="1" [marks]="speeds" ></gui-slider> </div> </div> `,})export class SliderMarksComponent { speeds: GuiSliderMark[] = [ { value: 0.5, label: 'Slow' }, { value: 1, label: 'Normal' }, 1.5, { value: 2, label: 'Fast' },];}<script setup lang="ts">import type { GuiSliderMark } from '@golemui/gui-components';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
const speeds: GuiSliderMark[] = [ { value: 0.5, label: 'Slow' }, { value: 1, label: 'Normal' }, 1.5, { value: 2, label: 'Fast' },];</script>
<template> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Brightness" :step="10" :value="60" :marks="true" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Playback speed" :minimum="0.5" :maximum="2" :step="0.25" :value="1" :marks.prop="speeds" ></gui-slider> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { GuiSliderMark } from '@golemui/gui-components';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
const speeds: GuiSliderMark[] = [ { value: 0.5, label: 'Slow' }, { value: 1, label: 'Normal' }, 1.5, { value: 2, label: 'Fast' },];
@customElement('slider-marks')export class SliderMarks extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Brightness" step="10" value="60" marks ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Playback speed" minimum="0.5" maximum="2" step="0.25" value="1" .marks=${speeds} ></gui-slider> </div> </div> `; }}<div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Brightness" step="10" value="60" marks ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Playback speed" minimum="0.5" maximum="2" step="0.25" value="1" marks='[{"value":0.5,"label":"Slow"},{"value":1,"label":"Normal"},1.5,{"value":2,"label":"Fast"}]' ></gui-slider> </div></div>The labels show as you write them: format doesn’t change them. Marks outside the bounds are
left out.
Number format
Section titled “Number format”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/slider';
@Component({ selector: 'app-slider-format', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Opacity" maximum="1" step="0.05" value="0.8" [format]="percent" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Monthly budget" maximum="5000" step="100" value="1500" [format]="euros" locale-id="de" ></gui-slider> </div> </div> `,})export class SliderFormatComponent { percent: Intl.NumberFormatOptions = { style: 'percent' }; euros: Intl.NumberFormatOptions = { style: 'currency', currency: 'EUR', maximumFractionDigits: 0,};}<script setup lang="ts">import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
const percent: Intl.NumberFormatOptions = { style: 'percent' };const euros: Intl.NumberFormatOptions = { style: 'currency', currency: 'EUR', maximumFractionDigits: 0,};</script>
<template> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Opacity" :maximum="1" :step="0.05" :value="0.8" :format.prop="percent" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Monthly budget" :maximum="5000" :step="100" :value="1500" :format.prop="euros" locale-id="de" ></gui-slider> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
const percent: Intl.NumberFormatOptions = { style: 'percent' };const euros: Intl.NumberFormatOptions = { style: 'currency', currency: 'EUR', maximumFractionDigits: 0,};
@customElement('slider-format')export class SliderFormat extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Opacity" maximum="1" step="0.05" value="0.8" .format=${percent} ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Monthly budget" maximum="5000" step="100" value="1500" .format=${euros} locale-id="de" ></gui-slider> </div> </div> `; }}<div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Opacity" maximum="1" step="0.05" value="0.8" format='{"style":"percent"}' ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Monthly budget" maximum="5000" step="100" value="1500" format='{"style":"currency","currency":"EUR","maximumFractionDigits":0}' locale-id="de" ></gui-slider> </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.
Where the value shows
Section titled “Where the value shows”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/slider';
@Component({ selector: 'app-slider-value-placement', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Next to the label" value="25" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="In a bubble" value="50" value-placement="bubble" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Both" value="75" value-placement="both" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Hidden" value="100" value-placement="hidden" ></gui-slider> </div> </div> `,})export class SliderValuePlacementComponent {}<script setup lang="ts">import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';</script>
<template> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Next to the label" :value="25" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="In a bubble" :value="50" value-placement="bubble" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Both" :value="75" value-placement="both" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Hidden" :value="100" value-placement="hidden" ></gui-slider> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
@customElement('slider-value-placement')export class SliderValuePlacement extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Next to the label" value="25" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="In a bubble" value="50" value-placement="bubble" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Both" value="75" value-placement="both" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Hidden" value="100" value-placement="hidden" ></gui-slider> </div> </div> `; }}<div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="Next to the label" value="25" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="In a bubble" value="50" value-placement="bubble" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Both" value="75" value-placement="both" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Hidden" value="100" value-placement="hidden" ></gui-slider> </div></div>Escape hides the bubble until the pointer and the focus leave the slider. See Accessibility.
Reacting to changes
Section titled “Reacting to changes”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%.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiSliderEvents } from '@golemui/gui-components';import '@golemui/gui-components/slider';
type SliderEvent<K extends keyof typeof GuiSliderEvents> = GuiEventMap< typeof GuiSliderEvents>[K];
@Component({ selector: 'app-slider-events', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-slider id="zoom-slider" label="Zoom" minimum="50" maximum="200" step="10" value="100" (gui-input)="onInput($event)" (gui-change)="onChange($event)" ></gui-slider> <p id="zoom-status" role="status"> Preview: {{ preview() }}%. Saved: {{ saved() }}%. </p> `,})export class SliderEventsComponent { preview = signal(100); saved = signal(100);
onInput(event: Event) { this.preview.set((event as SliderEvent<'gui-input'>).detail.value ?? 100); }
onChange(event: Event) { this.saved.set((event as SliderEvent<'gui-change'>).detail.value ?? 100); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
const preview = ref(100);const saved = ref(100);</script>
<template> <gui-slider id="zoom-slider" label="Zoom" :minimum="50" :maximum="200" :step="10" :value="100" @gui-input="preview = $event.detail.value ?? 100" @gui-change="saved = $event.detail.value ?? 100" ></gui-slider> <p id="zoom-status" role="status"> Preview: {{ preview }}%. Saved: {{ saved }}%. </p></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiEventMap, GuiSliderEvents } from '@golemui/gui-components';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
type SliderEvent<K extends keyof typeof GuiSliderEvents> = GuiEventMap< typeof GuiSliderEvents>[K];
@customElement('slider-events')export class SliderEvents extends LitElement { @state() private preview = 100; @state() private saved = 100;
protected override createRenderRoot() { return this; }
private onInput(event: SliderEvent<'gui-input'>) { this.preview = event.detail.value ?? 100; }
private onChange(event: SliderEvent<'gui-change'>) { this.saved = event.detail.value ?? 100; }
override render() { return html` <gui-slider id="zoom-slider" label="Zoom" minimum="50" maximum="200" step="10" value="100" @gui-input=${this.onInput} @gui-change=${this.onChange} ></gui-slider> <p id="zoom-status" role="status"> Preview: ${this.preview}%. Saved: ${this.saved}%. </p> `; }}<gui-slider id="zoom-slider" label="Zoom" minimum="50" maximum="200" step="10" value="100"></gui-slider><p id="zoom-status" role="status">Preview: 100%. Saved: 100%.</p>
<script type="module"> const zoom = document.querySelector('#zoom-slider'); let preview = 100; let saved = 100;
const show = () => { document.querySelector('#zoom-status').textContent = `Preview: ${preview}%. Saved: ${saved}%.`; };
zoom.addEventListener('gui-input', (event) => { preview = event.detail.value; show(); });
zoom.addEventListener('gui-change', (event) => { saved = event.detail.value; show(); });</script>Disabled and read-only
Section titled “Disabled and read-only”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/slider';
@Component({ selector: 'app-slider-disabled-readonly', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-slider label="Bass" value="30" disabled ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Treble" value="70" readonly ></gui-slider> </div> </div> `,})export class SliderDisabledReadonlyComponent {}<script setup lang="ts">import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-slider label="Bass" :value="30" :disabled="true" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Treble" :value="70" :readOnly="true" ></gui-slider> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
@customElement('slider-disabled-readonly')export class SliderDisabledReadonly extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-slider label="Bass" value="30" disabled ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Treble" value="70" readonly ></gui-slider> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-slider label="Bass" value="30" disabled ></gui-slider> </div> <div class="gui-grid__cell"> <gui-slider label="Treble" value="70" readonly ></gui-slider> </div></div>hint and errors work as on the text input.
In a form
Section titled “In a form”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.0is a value.- A
valueset from outside the bounds, such asvalue="150"withmaximum="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
valueset off a step, such asvalue="42"withstep="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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import '@golemui/gui-components/button';import '@golemui/gui-components/slider';
@Component({ selector: 'app-slider-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="survey" #form (submit)="onSubmit($event, form)"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="How likely are you to recommend us?" name="score" maximum="10" marks required ></gui-slider> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Send"></gui-button> </div> </div> <p id="survey-result" role="status">{{ result() }}</p> </form> `,})export class SliderNativeFormComponent { result = signal('');
onSubmit(event: SubmitEvent, form: HTMLFormElement) { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); this.result.set(`Submitted ${JSON.stringify(data)}`); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/button';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
const result = ref('');
function onSubmit(event: SubmitEvent) { if (!(event.currentTarget instanceof HTMLFormElement)) return; const data = Object.fromEntries(new FormData(event.currentTarget)); result.value = `Submitted ${JSON.stringify(data)}`;}</script>
<template> <form id="survey" @submit.prevent="onSubmit"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="How likely are you to recommend us?" name="score" :maximum="10" :marks="true" :required="true" ></gui-slider> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Send"></gui-button> </div> </div> <p id="survey-result" role="status">{{ result }}</p> </form></template>import { LitElement, html } from 'lit';import { customElement, query, state } from 'lit/decorators.js';import '@golemui/gui-components/button';import '@golemui/gui-components/slider';import '@golemui/gui-components/index.css';
@customElement('slider-native-form')export class SliderNativeForm extends LitElement { @query('#survey') private form!: HTMLFormElement; @state() private result = '';
protected override createRenderRoot() { return this; }
private onSubmit(event: SubmitEvent) { event.preventDefault(); const data = Object.fromEntries(new FormData(this.form)); this.result = `Submitted ${JSON.stringify(data)}`; }
override render() { return html` <form id="survey" @submit=${this.onSubmit}> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="How likely are you to recommend us?" name="score" maximum="10" marks required ></gui-slider> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Send"></gui-button> </div> </div> <p id="survey-result" role="status">${this.result}</p> </form> `; }}<form id="survey"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-slider label="How likely are you to recommend us?" name="score" maximum="10" marks required ></gui-slider> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Send"></gui-button> </div> </div> <p id="survey-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#survey');
form.addEventListener('submit', (event) => { event.preventDefault(); const data = Object.fromEntries(new FormData(form)); document.querySelector('#survey-result').textContent = `Submitted ${JSON.stringify(data)}`; });</script>How the elements submit, validate and reset is in Native forms.
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<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 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>| Class | Part |
|---|---|
gui-field | The element itself. Lines its parts up with the fields next to it in a gui-grid row. |
gui-label, gui-label__text | The label, and its text with the required marker. |
gui-widget-hint | The hint. |
gui-slider__value | The value at the end of the label’s line. Screen readers skip it: they read the thumb. |
gui-widget | The box around the control. |
gui-slider__control | The 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--empty | On the control while there is no value: the thumb takes a neutral border. |
gui-slider__control--dismissed | On the control after Escape hid the bubble. |
gui-slider__rail | The track. |
gui-slider__fill | The filled part of the track, from minimum to the thumb. |
gui-slider__ticks, gui-slider__tick | The tick marks, and one tick. |
gui-slider__tick--filled | On a tick in the filled part. |
gui-slider__input | The native range input. Its own track is transparent; the thumb is its ::-webkit-slider-thumb and ::-moz-range-thumb. |
gui-slider__bubble | The value over the thumb, with its arrow drawn as its ::before. |
gui-slider__footer | What goes under the control: the tick labels, then the errors. |
gui-slider__tick-labels | The labels of the marks. |
gui-slider__tick-label | One label. --first and --last line the labels at the ends up with the edges of the slider. |
gui-validator, gui-validator__error | The 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.
Tokens
Section titled “Tokens”The slider reads these design tokens. Set them on :root to change every slider, or on any
element to change the sliders inside it:
| Token | What it styles |
|---|---|
--gui-slider-thumb-size | The width and height of the thumb, 1.25rem by default. |
--gui-slider-track-size | The thickness of the track, 0.25rem by default. The ticks are twice as large. |
--gui-widget-size | The height of the control, as for a text field. |
--gui-border-strong | The track, the ticks, and the thumb’s border while there is no value. |
--gui-intent-primary | The filled part, the filled ticks and the thumb’s border. |
--gui-bg-default | The thumb. |
--gui-shadow-sm | The thumb’s shadow. |
--gui-shadow-focus | The ring around the thumb while it has keyboard focus or is dragged. |
--gui-transition-normal | How fast the ring shows, 0.2s when unset. |
--gui-tooltip-bg | The bubble’s background. Unset, it takes --gui-bg-inverse. |
--gui-tooltip-text | The bubble’s text. Unset, it takes --gui-text-inverse. |
--gui-radius-sm | The bubble’s corners. |
--gui-text-default | The label and the value. |
--gui-font-family | The font. |
--gui-font-sm | The text size of the label and the value. |
--gui-font-xs | The text size of the hint, the bubble, the tick labels and the errors. |
--gui-text-secondary | The hint and the tick labels. |
--gui-intent-error | The thumb’s border and the messages when there are errors. |
--gui-bg-disabled | The track, the ticks and the thumb when disabled. |
--gui-text-disabled | The filled part and the thumb’s border when disabled. |
--gui-space-1, --gui-space-2 | The space above the tick labels, the bubble’s padding and the gap before the value. |
/* 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.
Accessibility
Section titled “Accessibility”- Role. The thumb is a native
<input type="range">, so screen readers announce a slider with its minimum, maximum and value. - Name. The
labelnames the slider. Without a label the slider has no accessible name. - Value. The thumb reads its value through
aria-valuetext, in the slider’sformatandlocale-id, such as “80%” or “1.500 €”. Before there is a value, it reads “Not set”: translate it under the keynotSet. The value next to the label, the bubble, the ticks and their labels are hidden from screen readers. - Description and errors. The
hintis the slider’s description. While errors show, the thumb hasaria-invalid="true"and points at them witharia-errormessage, and screen readers announce new errors as they appear. - Required and read-only.
requiredsetsaria-requiredandreadonlysetsaria-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.
| Key | Action |
|---|---|
| Tab | Moves focus to the thumb, and away from it. |
| → ↑ | Moves one step up. |
| ← ↓ | Moves one step down. |
| Page Up Page Down | Moves a larger step up or down, which the browser sets: a tenth of the scale in Chrome. |
| Home End | Moves to minimum or maximum. |
| Escape | Hides 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.