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.
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} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';
@Component({ selector: 'app-range-slider-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-slider label="Price" maximum="500" step="10" [value]="price" [format]="dollars" ></gui-range-slider> `,})export class RangeSliderBasicComponent { price: GuiSliderRange = { start: 100, end: 300 }; dollars: Intl.NumberFormatOptions = { style: 'currency', currency: 'USD', maximumFractionDigits: 0,};}<script setup lang="ts">import type { GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';import '@golemui/gui-components/index.css';
const price: GuiSliderRange = { start: 100, end: 300 };const dollars: Intl.NumberFormatOptions = { style: 'currency', currency: 'USD', maximumFractionDigits: 0,};</script>
<template> <gui-range-slider label="Price" :maximum="500" :step="10" :value.prop="price" :format.prop="dollars" ></gui-range-slider></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';import '@golemui/gui-components/index.css';
const price: GuiSliderRange = { start: 100, end: 300 };const dollars: Intl.NumberFormatOptions = { style: 'currency', currency: 'USD', maximumFractionDigits: 0,};
@customElement('range-slider-basic')export class RangeSliderBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-slider label="Price" maximum="500" step="10" .value=${price} .format=${dollars} ></gui-range-slider> `; }}<gui-range-slider label="Price" maximum="500" step="10" value='{"start":100,"end":300}' format='{"style":"currency","currency":"USD","maximumFractionDigits":0}'></gui-range-slider>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.
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 })} /> );}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, GuiRangeSliderEvents } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';
type ChangeEvent = GuiEventMap<typeof GuiRangeSliderEvents>['gui-change'];
@Component({ selector: 'app-price-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-slider label="Price" name="price" maximum="500" [value]="price" (gui-change)="onChange($event)" ></gui-range-slider> `,})export class PriceFieldComponent { price = { start: 0, end: 500 };
onChange(event: Event) { this.price = (event as ChangeEvent).detail.value ?? { start: 0, end: 500 }; }}"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/range-slider';import '@golemui/gui-components/index.css';
const price = ref({ start: 0, end: 500 });</script>
<template> <gui-range-slider label="Price" name="price" maximum="500" :value="price" @gui-change="price = $event.detail.value ?? { start: 0, end: 500 }" /></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/range-slider';import '@golemui/gui-components/index.css';
@customElement('price-field')export class PriceField extends LitElement { @state() private price = { start: 0, end: 500 };
protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-slider label="Price" name="price" maximum="500" .value=${this.price} @gui-change=${(event: CustomEvent) => (this.price = event.detail.value)} ></gui-range-slider> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-range-slider label="Price" name="price" maximum="500"></gui-range-slider>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/range-slider';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-range-slider');
element.addEventListener('gui-change', (event) => { console.log(event.detail.value);});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. |
Examples
Section titled “Examples”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.
The value
Section titled “The value”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.
Moving the thumbs
Section titled “Moving the thumbs”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.
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" /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { GuiSliderMark, GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';
@Component({ selector: 'app-range-slider-marks-bubble', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-slider label="Age" minimum="18" maximum="78" [value]="ages" [marks]="decades" value-placement="bubble" ></gui-range-slider> `,})export class RangeSliderMarksBubbleComponent { ages: GuiSliderRange = { start: 25, end: 40 }; 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' },];}<script setup lang="ts">import type { GuiSliderMark, GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';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' },];</script>
<template> <gui-range-slider label="Age" :minimum="18" :maximum="78" :value.prop="ages" :marks.prop="decades" value-placement="bubble" ></gui-range-slider></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { GuiSliderMark, GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';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' },];
@customElement('range-slider-marks-bubble')export class RangeSliderMarksBubble extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-slider label="Age" minimum="18" maximum="78" .value=${ages} .marks=${decades} value-placement="bubble" ></gui-range-slider> `; }}<gui-range-slider label="Age" minimum="18" maximum="78" value='{"start":25,"end":40}' marks='[{"value":18,"label":"18"},{"value":30,"label":"30"},{"value":42,"label":"42"},{"value":54,"label":"54"},{"value":66,"label":"66"},{"value":78,"label":"78"}]' value-placement="bubble"></gui-range-slider>Thumb names
Section titled “Thumb names”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.
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} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { GuiSliderMark, GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';
@Component({ selector: 'app-range-slider-thumb-names', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-slider label="Departure time" maximum="24" [value]="departure" start-label="Earliest" end-label="Latest" [marks]="hours" ></gui-range-slider> `,})export class RangeSliderThumbNamesComponent { departure: GuiSliderRange = { start: 8, end: 14 }; hours: GuiSliderMark[] = [ { value: 0, label: '00:00' }, { value: 12, label: '12:00' }, { value: 24, label: '24:00' },];}<script setup lang="ts">import type { GuiSliderMark, GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';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' },];</script>
<template> <gui-range-slider label="Departure time" :maximum="24" :value.prop="departure" start-label="Earliest" end-label="Latest" :marks.prop="hours" ></gui-range-slider></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { GuiSliderMark, GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';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' },];
@customElement('range-slider-thumb-names')export class RangeSliderThumbNames extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-slider label="Departure time" maximum="24" .value=${departure} start-label="Earliest" end-label="Latest" .marks=${hours} ></gui-range-slider> `; }}<gui-range-slider label="Departure time" maximum="24" value='{"start":8,"end":14}' start-label="Earliest" end-label="Latest" marks='[{"value":0,"label":"00:00"},{"value":12,"label":"12:00"},{"value":24,"label":"24:00"}]'></gui-range-slider>To translate the default names for every range slider, use the keys minimum and maximum. See
i18n.
Reacting to changes
Section titled “Reacting to changes”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.
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> </> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiRangeSliderEvents, GuiSliderRange,} from '@golemui/gui-components';import '@golemui/gui-components/range-slider';
type RangeSliderEvent<K extends keyof typeof GuiRangeSliderEvents> = GuiEventMap<typeof GuiRangeSliderEvents>[K];
@Component({ selector: 'app-range-slider-events', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-slider id="hours-slider" label="Opening hours" minimum="6" maximum="24" [value]="hours()" value-placement="hidden" (gui-input)="onInput($event)" ></gui-range-slider> <p id="hours-status" role="status"> Open from {{ hours().start }}:00 to {{ hours().end }}:00. </p> `,})export class RangeSliderEventsComponent { hours = signal<GuiSliderRange>({ start: 9, end: 18 });
onInput(event: Event) { const value = (event as RangeSliderEvent<'gui-input'>).detail.value; if (value) this.hours.set(value); }}<script setup lang="ts">import { ref } from 'vue';import type { GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';import '@golemui/gui-components/index.css';
const hours = ref<GuiSliderRange>({ start: 9, end: 18 });</script>
<template> <gui-range-slider id="hours-slider" label="Opening hours" :minimum="6" :maximum="24" :value.prop="hours" value-placement="hidden" @gui-input="hours = $event.detail.value ?? hours" ></gui-range-slider> <p id="hours-status" role="status"> Open from {{ hours.start }}:00 to {{ hours.end }}:00. </p></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiEventMap, GuiRangeSliderEvents, GuiSliderRange,} from '@golemui/gui-components';import '@golemui/gui-components/range-slider';import '@golemui/gui-components/index.css';
type RangeSliderEvent<K extends keyof typeof GuiRangeSliderEvents> = GuiEventMap<typeof GuiRangeSliderEvents>[K];
@customElement('range-slider-events')export class RangeSliderEvents extends LitElement { @state() private hours: GuiSliderRange = { start: 9, end: 18 };
protected override createRenderRoot() { return this; }
private onInput(event: RangeSliderEvent<'gui-input'>) { if (event.detail.value) this.hours = event.detail.value; }
override render() { return html` <gui-range-slider id="hours-slider" label="Opening hours" minimum="6" maximum="24" .value=${this.hours} value-placement="hidden" @gui-input=${this.onInput} ></gui-range-slider> <p id="hours-status" role="status"> Open from ${this.hours.start}:00 to ${this.hours.end}:00. </p> `; }}<gui-range-slider id="hours-slider" label="Opening hours" minimum="6" maximum="24" value='{"start":9,"end":18}' value-placement="hidden"></gui-range-slider><p id="hours-status" role="status">Open from 9:00 to 18:00.</p>
<script type="module"> const hours = document.querySelector('#hours-slider');
hours.addEventListener('gui-input', (event) => { const { start, end } = event.detail.value; document.querySelector('#hours-status').textContent = `Open from ${start}:00 to ${end}:00.`; });</script>gui-blur fires when focus leaves the range slider: moving from one thumb to the other doesn’t
count.
Disabled and read-only
Section titled “Disabled and read-only”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';
@Component({ selector: 'app-range-slider-disabled-readonly', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-range-slider label="Quiet hours" maximum="24" [value]="quiet" disabled ></gui-range-slider> </div> <div class="gui-grid__cell"> <gui-range-slider label="Work hours" maximum="24" [value]="work" readonly ></gui-range-slider> </div> </div> `,})export class RangeSliderDisabledReadonlyComponent { quiet: GuiSliderRange = { start: 22, end: 24 }; work: GuiSliderRange = { start: 9, end: 17 };}<script setup lang="ts">import type { GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';import '@golemui/gui-components/index.css';
const quiet: GuiSliderRange = { start: 22, end: 24 };const work: GuiSliderRange = { start: 9, end: 17 };</script>
<template> <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-range-slider label="Quiet hours" :maximum="24" :value.prop="quiet" :disabled="true" ></gui-range-slider> </div> <div class="gui-grid__cell"> <gui-range-slider label="Work hours" :maximum="24" :value.prop="work" :readOnly="true" ></gui-range-slider> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { GuiSliderRange } from '@golemui/gui-components';import '@golemui/gui-components/range-slider';import '@golemui/gui-components/index.css';
const quiet: GuiSliderRange = { start: 22, end: 24 };const work: GuiSliderRange = { start: 9, end: 17 };
@customElement('range-slider-disabled-readonly')export class RangeSliderDisabledReadonly extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-range-slider label="Quiet hours" maximum="24" .value=${quiet} disabled ></gui-range-slider> </div> <div class="gui-grid__cell"> <gui-range-slider label="Work hours" maximum="24" .value=${work} readonly ></gui-range-slider> </div> </div> `; }}<div class="gui-grid gui-grid--row"> <div class="gui-grid__cell"> <gui-range-slider label="Quiet hours" maximum="24" value='{"start":22,"end":24}' disabled ></gui-range-slider> </div> <div class="gui-grid__cell"> <gui-range-slider label="Work hours" maximum="24" value='{"start":9,"end":17}' readonly ></gui-range-slider> </div></div>In a form
Section titled “In a form”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import '@golemui/gui-components/button';import '@golemui/gui-components/range-slider';
@Component({ selector: 'app-range-slider-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="search" #form (submit)="onSubmit($event, form)"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-slider label="Nights" name="nights" minimum="1" maximum="14" marks required ></gui-range-slider> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Search"></gui-button> </div> </div> <p id="search-result" role="status">{{ result() }}</p> </form> `,})export class RangeSliderNativeFormComponent { result = signal('');
onSubmit(event: SubmitEvent, form: HTMLFormElement) { event.preventDefault(); const nights = new FormData(form).get('nights'); this.result.set(`Submitted ${nights}`); }}<script setup lang="ts">import { ref } from 'vue';import '@golemui/gui-components/button';import '@golemui/gui-components/range-slider';import '@golemui/gui-components/index.css';
const result = ref('');
function onSubmit(event: SubmitEvent) { if (!(event.currentTarget instanceof HTMLFormElement)) return; const nights = new FormData(event.currentTarget).get('nights'); result.value = `Submitted ${nights}`;}</script>
<template> <form id="search" @submit.prevent="onSubmit"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-slider label="Nights" name="nights" :minimum="1" :maximum="14" :marks="true" :required="true" ></gui-range-slider> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Search"></gui-button> </div> </div> <p id="search-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/range-slider';import '@golemui/gui-components/index.css';
@customElement('range-slider-native-form')export class RangeSliderNativeForm extends LitElement { @query('#search') private form!: HTMLFormElement; @state() private result = '';
protected override createRenderRoot() { return this; }
private onSubmit(event: SubmitEvent) { event.preventDefault(); const nights = new FormData(this.form).get('nights'); this.result = `Submitted ${nights}`; }
override render() { return html` <form id="search" @submit=${this.onSubmit}> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-slider label="Nights" name="nights" minimum="1" maximum="14" marks required ></gui-range-slider> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Search"></gui-button> </div> </div> <p id="search-result" role="status">${this.result}</p> </form> `; }}<form id="search"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-slider label="Nights" name="nights" minimum="1" maximum="14" marks required ></gui-range-slider> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Search"></gui-button> </div> </div> <p id="search-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#search');
form.addEventListener('submit', (event) => { event.preventDefault(); const nights = new FormData(form).get('nights'); document.querySelector('#search-result').textContent = `Submitted ${nights}`; });</script>How the elements submit, validate and reset is in Native forms.
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<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 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:
| Class | Part |
|---|---|
gui-slider__control--range | On the control. It takes the presses on the track; each input takes the pointer on its thumb only. |
gui-slider__fill | The filled part, between the two thumbs. |
gui-slider__tick--filled | On a tick between the two thumbs. |
gui-visually-hidden | The 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.
Tokens
Section titled “Tokens”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:
/* 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.
Accessibility
Section titled “Accessibility”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-labelandend-label, or translate them under the keysminimumandmaximum. - 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,
requiredandreadonlyapply to both thumbs: each one has the description,aria-invalid,aria-requiredandaria-readonly. - Focus. Both thumbs are tab stops, the low one first.
gui-blurfires only when focus leaves both.
| Key | Action |
|---|---|
| Tab | Moves 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 Down | Move the focused thumb a larger step, up to the other thumb. |
| Home End | Move the focused thumb as far as it goes: to the end of the scale, or to the other thumb. |
| Escape | Hides the bubbles, without closing a dialog around the range slider. |
What every element shares, such as translating the default names, is in Accessibility.