Time range
<gui-range-time> is a field to type time ranges. The user types a start and an end time part by
part, presses Enter, and the range becomes a pill. The field holds as many ranges as
the user adds.
import { GuiRangeTimeInput } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function RangeTimeBasic() { return <GuiRangeTimeInput label="Opening hours" />;}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/range-time-input';
@Component({ selector: 'app-range-time-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: `<gui-range-time label="Opening hours"></gui-range-time>`,})export class RangeTimeBasicComponent {}<script setup lang="ts">import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';</script>
<template> <gui-range-time label="Opening hours"></gui-range-time></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
@customElement('range-time-basic')export class RangeTimeBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html`<gui-range-time label="Opening hours"></gui-range-time>`; }}<gui-range-time label="Opening hours"></gui-range-time>To pick the times from lists instead, use the Time range picker. For a single time, use the Time input.
The element’s entry point is @golemui/gui-components/range-time-input, which registers
<gui-range-time> and the pills it renders. 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 { GuiRangeTime } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function HoursField() { const [hours, setHours] = useState([]);
return ( <GuiRangeTime label="Opening hours" name="hours" value={hours} onGuiChange={(event) => setHours(event.detail.value ?? [])} /> );}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, GuiRangeTimeEvents } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';
type ChangeEvent = GuiEventMap<typeof GuiRangeTimeEvents>['gui-change'];
@Component({ selector: 'app-hours-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-time label="Opening hours" name="hours" [value]="hours" (gui-change)="onChange($event)" ></gui-range-time> `,})export class HoursFieldComponent { hours = [];
onChange(event: Event) { this.hours = (event as ChangeEvent).detail.value ?? []; }}"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-time-input';import '@golemui/gui-components/index.css';
const hours = ref([]);</script>
<template> <gui-range-time label="Opening hours" name="hours" :value="hours" @gui-change="hours = $event.detail.value ?? []" /></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-time-input';import '@golemui/gui-components/index.css';
@customElement('hours-field')export class HoursField extends LitElement { @state() private hours = [];
protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-time label="Opening hours" name="hours" .value=${this.hours} @gui-change=${(event: CustomEvent) => (this.hours = event.detail.value)} ></gui-range-time> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-range-time label="Opening hours" name="hours"></gui-range-time>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-range-time');
element.addEventListener('gui-change', (event) => { console.log(event.detail.value);});Properties
| Property | Type | Default | Description |
|---|---|---|---|
localeId
attribute locale-id | string | – | BCP 47 locale for formatting and parsing, such as en-US or es. |
showErrors
attribute show-errors | boolean | true | Whether the element renders its own error list. Elements that embed it turn it off and show the errors themselves. |
showHint
attribute show-hint | boolean | true | Whether the element renders its hint. Elements that embed it turn it off and show the hint themselves: aria-describedby still points at the hint by its id. |
icon | string | '' | Icon class name shown inside the control, for example from an icon font. |
hourAriaLabel
attribute hour-aria-label | string | – | Accessible name of the hour part. An empty value keeps the default. |
minuteAriaLabel
attribute minute-aria-label | string | – | Accessible name of the minute part. An empty value keeps the default. |
dayPeriodAriaLabel
attribute day-period-aria-label | string | – | Accessible name of the AM/PM part. An empty value keeps the default. |
hourFormat
attribute hour-format | HourFormat | – | 12- or 24-hour clock. Defaults to the locale's. |
minuteStep
attribute minute-step | number | 1 | Minutes between the times offered in the list. |
minTime
attribute min-time | string | – | Earliest selectable time, as an ISO time (HH:mm:ss). |
maxTime
attribute max-time | string | – | Latest selectable time, as an ISO time (HH:mm:ss). |
minTimeMessage
attribute min-time-message | string | – | Error for a time before minTime. |
maxTimeMessage
attribute max-time-message | string | – | Error for a time after maxTime. |
value | TimeRange[] | [] | The time ranges, as { start, end } ISO times. |
rangeOrderMessage
attribute range-order-message | string | – | Error when the end time is not after the start time. |
removePillAriaLabel
attribute remove-pill-aria-label | string | – | Accessible name of the remove button of each range pill. An empty value keeps the default. |
startTimeAriaLabel
attribute start-time-aria-label | string | – | Accessible name of the start time field. An empty value removes it. |
endTimeAriaLabel
attribute end-time-aria-label | string | – | Accessible name of the end time field. An empty value removes it. |
separator | string | – | Text shown between the start and end of a range. |
allowCustomTime
attribute allow-custom-time | boolean | – | Allows typing any time, not only picking one from the list. |
disabledRanges
attribute disabled-ranges | TimeRange[] | – | Times that cannot be picked, as { start, end } ISO time ranges. |
disabledRangeMessage
attribute disabled-range-message | string | – | Error for a time inside disabledRanges. |
incompleteMessage
attribute incomplete-message | string | – | Error when focus leaves a partly filled value. |
allowEdit
attribute allow-edit | boolean | false | Opt-in select → edit → confirm flow on the pills. |
editLabel
attribute edit-label | string | – | Tooltip of the edit button of a range pill. |
editAriaLabel
attribute edit-aria-label | string | – | Hint that a range pill can be edited. {label} is the range. |
confirmEditLabel
attribute confirm-edit-label | string | – | Tooltip of the confirm button of a range being edited. |
cancelEditLabel
attribute cancel-edit-label | string | – | Tooltip of the cancel button of a range being edited. |
editStartedMessage
attribute edit-started-message | string | – | Announcement when editing a range starts. {label} is the range. |
editCommittedMessage
attribute edit-committed-message | string | – | Announcement when an edited range is saved. {label} is the new range. |
editCancelledMessage
attribute edit-cancelled-message | string | – | Announcement when editing a range is cancelled. |
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 changed the value. detail.value is the new value. |
gui-change | The user added, removed or finished editing a range. detail.value is the list of ranges. |
gui-blur | Focus left the control. |
gui-focus | One of the parts of the field received focus. |
gui-input-error | The element rejected what the user entered, such as an impossible date or a value out of bounds. detail.message is the error; show it through errors. |
gui-parts-change | The typed parts changed before they form a complete value, for a host that mirrors them. |
gui-edit-state-change | Editing a range in place started, changed selection or ended. detail has the selected range and whether it is editing. |
gui-range-click | The user clicked a range pill. detail.range is the range. |
gui-dropdown-toggle | The count of ranges opened or closed its dropdown. detail.open is the new state. |
CSS custom properties
| Property | Default | Description |
|---|---|---|
--gui-pill-height | – | Height of each pill. |
--gui-pill-font-size | – | Font size of the pill text. |
--gui-pill-action-size | – | Size of the icons inside a pill. |
--gui-pill-action-hit | – | Clickable area of the buttons inside a pill. |
Examples
Section titled “Examples”The value
Section titled “The value”value is a list of ranges. Each range is an object with a start and an end, both ISO times
(HH:mm:ss). As an attribute, value is a JSON array.
import { GuiRangeTimeInput } from '@golemui/gui-components/react';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const officeHours: TimeRange[] = [ { start: '09:00:00', end: '13:00:00' }, { start: '15:00:00', end: '18:30:00' },];
export function RangeTimeValue() { return <GuiRangeTimeInput label="Office hours" value={officeHours} />;}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';
@Component({ selector: 'app-range-time-value', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: `<gui-range-time label="Office hours" [value]="hours" ></gui-range-time>`,})export class RangeTimeValueComponent { hours: TimeRange[] = [ { start: '09:00:00', end: '13:00:00' }, { start: '15:00:00', end: '18:30:00' }, ];}<script setup lang="ts">import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const value: TimeRange[] = [ { start: '09:00:00', end: '13:00:00' }, { start: '15:00:00', end: '18:30:00' },];</script>
<template> <gui-range-time label="Office hours" :value.prop="value"></gui-range-time></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const officeHours: TimeRange[] = [ { start: '09:00:00', end: '13:00:00' }, { start: '15:00:00', end: '18:30:00' },];
@customElement('range-time-value')export class RangeTimeValue extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-time label="Office hours" .value=${officeHours} ></gui-range-time> `; }}<gui-range-time label="Office hours" value='[{"start":"09:00:00","end":"13:00:00"},{"start":"15:00:00","end":"18:30:00"}]'></gui-range-time>- The end must come after the start: a range within one day, never across midnight. A range typed the other way round is rejected with an error, not swapped.
- A range that overlaps or touches another one merges with it: 9:00 to 13:00 and 13:00 to 15:00 become 9:00 to 15:00.
- The pills show in time order.
gui-change fires each time the user adds or removes a range, with the whole list in
event.detail.value.
hour-format sets a 12 or 24-hour clock. Without it, the field follows locale-id. On a
12-hour clock, the AM/PM part is a button: click it, or press Space, ↑ or
↓ on it, to switch.
import { GuiRangeTimeInput } from '@golemui/gui-components/react';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const shift: TimeRange[] = [{ start: '07:00:00', end: '15:00:00' }];
export function RangeTimeHourFormat() { return ( <div className="gui-grid"> <div className="gui-grid__cell"> <GuiRangeTimeInput label="Shifts, 24-hour clock" hourFormat="24" value={shift} /> </div> <div className="gui-grid__cell"> <GuiRangeTimeInput label="Shifts, 12-hour clock" hourFormat="12" value={shift} /> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';
@Component({ selector: 'app-range-time-hour-format', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Shifts, 24-hour clock" hour-format="24" [value]="shift" ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-range-time label="Shifts, 12-hour clock" hour-format="12" [value]="shift" ></gui-range-time> </div> </div> `,})export class RangeTimeHourFormatComponent { shift: TimeRange[] = [{ start: '07:00:00', end: '15:00:00' }];}<script setup lang="ts">import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const value: TimeRange[] = [{ start: '07:00:00', end: '15:00:00' }];</script>
<template> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Shifts, 24-hour clock" hour-format="24" :value.prop="value" ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-range-time label="Shifts, 12-hour clock" hour-format="12" :value.prop="value" ></gui-range-time> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const shift: TimeRange[] = [{ start: '07:00:00', end: '15:00:00' }];
@customElement('range-time-hour-format')export class RangeTimeHourFormat extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Shifts, 24-hour clock" hour-format="24" .value=${shift} ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-range-time label="Shifts, 12-hour clock" hour-format="12" .value=${shift} ></gui-range-time> </div> </div> `; }}<div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Shifts, 24-hour clock" hour-format="24" value='[{"start":"07:00:00","end":"15:00:00"}]' ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-range-time label="Shifts, 12-hour clock" hour-format="12" value='[{"start":"07:00:00","end":"15:00:00"}]' ></gui-range-time> </div></div>locale-id also sets the format of the pills. See i18n.
Bounds and disabled times
Section titled “Bounds and disabled times”min-time and max-time limit the times the user can enter, as ISO times. disabled-ranges
takes a JSON list of { start, end } time ranges the user can’t use: a range that overlaps one,
even only at an end, is rejected. 10:00 AM to 1:00 PM is rejected below, because it touches the
lunch break.
import { useState } from 'react';import { GuiRangeTimeInput } from '@golemui/gui-components/react';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const lunch: TimeRange[] = [{ start: '13:00:00', end: '14:00:00' }];
export function RangeTimeBounds() { const [errors, setErrors] = useState<string[]>([]);
return ( <GuiRangeTimeInput id="delivery-window" label="Delivery windows" hint="Between 8:00 AM and 8:00 PM. No deliveries from 1:00 PM to 2:00 PM." hourFormat="12" minTime="08:00:00" maxTime="20:00:00" disabledRanges={lunch} errors={errors} onGuiInputError={(event) => setErrors([event.detail.message])} onGuiInput={() => setErrors([])} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { GuiEventMap, GuiRangeTimeInputEvents, TimeRange,} from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';
type RangeTimeEvent<K extends keyof typeof GuiRangeTimeInputEvents> = GuiEventMap<typeof GuiRangeTimeInputEvents>[K];
@Component({ selector: 'app-range-time-bounds', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-time id="delivery-window" label="Delivery windows" hint="Between 8:00 AM and 8:00 PM. No deliveries from 1:00 PM to 2:00 PM." hour-format="12" min-time="08:00:00" max-time="20:00:00" [disabledRanges]="lunch" [errors]="errors()" (gui-input-error)="onInputError($event)" (gui-input)="errors.set([])" ></gui-range-time> `,})export class RangeTimeBoundsComponent { lunch: TimeRange[] = [{ start: '13:00:00', end: '14:00:00' }];
errors = signal<string[]>([]);
onInputError(event: Event) { this.errors.set([ (event as RangeTimeEvent<'gui-input-error'>).detail.message, ]); }}<script setup lang="ts">import { ref } from 'vue';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const disabledRanges: TimeRange[] = [{ start: '13:00:00', end: '14:00:00' }];const errors = ref<string[]>([]);</script>
<template> <gui-range-time id="delivery-window" label="Delivery windows" hint="Between 8:00 AM and 8:00 PM. No deliveries from 1:00 PM to 2:00 PM." hour-format="12" min-time="08:00:00" max-time="20:00:00" :disabledRanges.prop="disabledRanges" :errors.prop="errors" @gui-input-error="errors = [$event.detail.message]" @gui-input="errors = []" ></gui-range-time></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { GuiEventMap, GuiRangeTimeInputEvents, TimeRange,} from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
type RangeTimeEvent<K extends keyof typeof GuiRangeTimeInputEvents> = GuiEventMap<typeof GuiRangeTimeInputEvents>[K];
const lunch: TimeRange[] = [{ start: '13:00:00', end: '14:00:00' }];
@customElement('range-time-bounds')export class RangeTimeBounds extends LitElement { @state() private errors: string[] = [];
protected override createRenderRoot() { return this; }
private onInputError(event: RangeTimeEvent<'gui-input-error'>) { this.errors = [event.detail.message]; }
private onInput() { this.errors = []; }
override render() { return html` <gui-range-time id="delivery-window" label="Delivery windows" hint="Between 8:00 AM and 8:00 PM. No deliveries from 1:00 PM to 2:00 PM." hour-format="12" min-time="08:00:00" max-time="20:00:00" .disabledRanges=${lunch} .errors=${this.errors} @gui-input-error=${this.onInputError} @gui-input=${this.onInput} ></gui-range-time> `; }}<gui-range-time id="delivery-window" label="Delivery windows" hint="Between 8:00 AM and 8:00 PM. No deliveries from 1:00 PM to 2:00 PM." hour-format="12" min-time="08:00:00" max-time="20:00:00" disabled-ranges='[{"start":"13:00:00","end":"14:00:00"}]'></gui-range-time>
<script type="module"> const delivery = document.querySelector('#delivery-window');
delivery.addEventListener('gui-input-error', (event) => { delivery.errors = [event.detail.message]; }); delivery.addEventListener('gui-input', () => { delivery.errors = []; });</script>When the field rejects a range, it fires gui-input-error with the message in
event.detail.message. It doesn’t show the message itself: the example sets it as errors and
clears it on the next gui-input, which fires once the problem is fixed. Change the messages with
min-time-message, max-time-message, range-order-message, disabled-range-message and
incomplete-message.
Editing ranges
Section titled “Editing ranges”allow-edit lets the user change a range they already added. Select a pill, then press
F2 or E, or use its edit button: the range loads into the parts and the pill
shows the change as the user types. Enter or the confirm button saves it, and
Escape or the cancel button keeps the old range. Screen readers announce each step.
import { GuiRangeTimeInput } from '@golemui/gui-components/react';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const officeHours: TimeRange[] = [ { start: '09:00:00', end: '13:00:00' }, { start: '15:00:00', end: '18:30:00' },];
export function RangeTimeEdit() { return ( <GuiRangeTimeInput label="Office hours" allowEdit value={officeHours} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';
@Component({ selector: 'app-range-time-edit', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-time label="Office hours" allow-edit [value]="hours" ></gui-range-time> `,})export class RangeTimeEditComponent { hours: TimeRange[] = [ { start: '09:00:00', end: '13:00:00' }, { start: '15:00:00', end: '18:30:00' }, ];}<script setup lang="ts">import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const value: TimeRange[] = [ { start: '09:00:00', end: '13:00:00' }, { start: '15:00:00', end: '18:30:00' },];</script>
<template> <gui-range-time label="Office hours" :allow-edit="true" :value.prop="value" ></gui-range-time></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const officeHours: TimeRange[] = [ { start: '09:00:00', end: '13:00:00' }, { start: '15:00:00', end: '18:30:00' },];
@customElement('range-time-edit')export class RangeTimeEdit extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-time label="Office hours" allow-edit .value=${officeHours} ></gui-range-time> `; }}<gui-range-time label="Office hours" allow-edit value='[{"start":"09:00:00","end":"13:00:00"},{"start":"15:00:00","end":"18:30:00"}]'></gui-range-time>Disabled and read-only
Section titled “Disabled and read-only”A disabled field can’t be focused or changed, and a form doesn’t submit it. A readonly field
shows its ranges but doesn’t let the user add or remove one, and a form submits its value.
import { GuiRangeTimeInput } from '@golemui/gui-components/react';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const lateEvening: TimeRange[] = [{ start: '22:00:00', end: '23:30:00' }];
export function RangeTimeDisabledReadonly() { return ( <div className="gui-grid"> <div className="gui-grid__cell"> <GuiRangeTimeInput label="Quiet hours" value={lateEvening} disabled /> </div> <div className="gui-grid__cell"> <GuiRangeTimeInput label="Booked hours" value={lateEvening} readOnly /> </div> </div> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';
@Component({ selector: 'app-range-time-disabled-readonly', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Quiet hours" [value]="evening" disabled ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-range-time label="Booked hours" [value]="evening" readonly ></gui-range-time> </div> </div> `,})export class RangeTimeDisabledReadonlyComponent { evening: TimeRange[] = [{ start: '22:00:00', end: '23:30:00' }];}<script setup lang="ts">import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const value: TimeRange[] = [{ start: '22:00:00', end: '23:30:00' }];</script>
<template> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Quiet hours" :value.prop="value" :disabled="true" ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-range-time label="Booked hours" :value.prop="value" :readOnly="true" ></gui-range-time> </div> </div></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { TimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const quietHours: TimeRange[] = [{ start: '22:00:00', end: '23:30:00' }];
@customElement('range-time-disabled-readonly')export class RangeTimeDisabledReadonly extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Quiet hours" .value=${quietHours} disabled ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-range-time label="Booked hours" .value=${quietHours} readonly ></gui-range-time> </div> </div> `; }}<div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Quiet hours" value='[{"start":"22:00:00","end":"23:30:00"}]' disabled ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-range-time label="Booked hours" value='[{"start":"22:00:00","end":"23:30:00"}]' readonly ></gui-range-time> </div></div>In a form
Section titled “In a form”With a name, <gui-range-time> submits one entry per range under that name. Each entry is the
range as JSON, such as {"start":"08:00:00","end":"09:00:00"}. Read them all with
FormData.getAll(). required blocks the submit while the field has no range.
import { useState } from 'react';import { GuiButton, GuiRangeTimeInput } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function RangeTimeNativeForm() { const [result, setResult] = useState('');
return ( <form id="hours-form" onSubmit={(event) => { event.preventDefault(); const ranges = new FormData(event.currentTarget).getAll('hours'); setResult(`Submitted ${ranges.join(', ')}`); }} > <div className="gui-grid"> <div className="gui-grid__cell"> <GuiRangeTimeInput label="Store hours" name="hours" hourFormat="24" required /> </div> <div className="gui-grid__cell"> <GuiButton type="submit" label="Save hours" /> </div> </div> <p role="status">{result}</p> </form> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import '@golemui/gui-components/button';import '@golemui/gui-components/range-time-input';
@Component({ selector: 'app-range-time-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="hours-form" #form (submit)="onSubmit($event, form)"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Store hours" name="hours" hour-format="24" required ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Save hours"></gui-button> </div> </div> <p id="hours-result" role="status">{{ result() }}</p> </form> `,})export class RangeTimeNativeFormComponent { result = signal('');
onSubmit(event: Event, form: HTMLFormElement) { event.preventDefault(); const ranges = new FormData(form).getAll('hours'); this.result.set(`Submitted ${ranges.join(', ')}`); }}<script setup lang="ts">import { ref, useTemplateRef } from 'vue';import '@golemui/gui-components/button';import '@golemui/gui-components/range-time-input';import '@golemui/gui-components/index.css';
const form = useTemplateRef('form');const result = ref('');
function onSubmit() { if (!form.value) return; const ranges = new FormData(form.value).getAll('hours'); result.value = `Submitted ${ranges.join(', ')}`;}</script>
<template> <form id="hours-form" ref="form" @submit.prevent="onSubmit"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Store hours" name="hours" hour-format="24" :required="true" ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Save hours"></gui-button> </div> </div> <p id="hours-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-time-input';import '@golemui/gui-components/index.css';
@customElement('range-time-native-form')export class RangeTimeNativeForm extends LitElement { @query('form') private form!: HTMLFormElement; @state() private ranges?: FormDataEntryValue[];
protected override createRenderRoot() { return this; }
private onSubmit(event: SubmitEvent) { event.preventDefault(); this.ranges = new FormData(this.form).getAll('hours'); }
override render() { return html` <form id="hours-form" @submit=${this.onSubmit}> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Store hours" name="hours" hour-format="24" required ></gui-range-time> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Save hours"></gui-button> </div> </div> <p role="status"> ${this.ranges && `Submitted ${this.ranges.join(', ')}`} </p> </form> `; }}<form id="hours-form"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-time label="Store hours" name="hours" hour-format="24" required></gui-range-time> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Save hours"></gui-button> </div> </div> <p id="hours-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#hours-form');
form.addEventListener('submit', (event) => { event.preventDefault(); const ranges = new FormData(form).getAll('hours'); document.querySelector('#hours-result').textContent = `Submitted ${ranges.join(', ')}`; });</script>A range that starts before min-time, ends after max-time or overlaps disabled-ranges blocks
the submit too, even one you set from code. The browser shows the same message the field gives the
user, such as the disabled-range-message.
How the elements submit, validate and reset is in Native forms.
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<gui-range-time> renders into the light DOM, so you can style every part with plain CSS. These
are the parts and their public classes, on a 12-hour clock with two ranges:
<gui-range-time class="gui-field"> <span class="gui-label" id="hours_label"> <span class="gui-label__text">Office hours</span> </span> <div class="gui-widget"> <div role="group" class="gui-widget-input gui-parts-ring gui-range-time-input" aria-labelledby="hours_label" > <gui-pills class="gui-range-time-input__pills"> <div class="gui-pills__strip-wrapper"> <div class="gui-pills__strip" role="toolbar" aria-label="Selected time ranges" > <button type="button" class="gui-pills__pill gui-pills__pill--clickable" aria-label="09:00 AM - 01:00 PM" aria-description="Remove time" > <span class="gui-pills__pill-text">09:00 AM - 01:00 PM</span> <span class="gui-pills__pill-remove" aria-hidden="true"> <svg>…</svg> </span> </button> <!-- The other range --> </div> </div> <div class="gui-pills__compact"> <button type="button" class="gui-pills__count" aria-haspopup="true" aria-label="2 time ranges" > 2 </button> </div> </gui-pills> <div class="gui-range-time-input__inputs"> <div class="gui-parts gui-range-time-input__field" role="group" aria-label="Start time" > <div dir="ltr" class="gui-parts__time-cluster gui-range-time-input__time-cluster" > <div class="gui-parts__touch-target gui-range-time-input__touch-target" > <input type="text" inputmode="numeric" role="spinbutton" class="gui-parts__part gui-range-time-input__part" placeholder="hh" aria-label="Hour" aria-valuemin="1" aria-valuemax="12" /> <div class="gui-parts__visual-underline gui-range-time-input__visual-underline" ></div> </div> <span class="gui-range-time-input__separator">:</span> <!-- The minute part --> </div> <span class="gui-range-time-input__separator"> </span> <div class="gui-parts__touch-target gui-range-time-input__touch-target" > <button type="button" class="gui-parts__part gui-parts__dayperiod gui-range-time-input__part gui-range-time-input__dayperiod" aria-label="AM/PM" > AM </button> <div class="gui-parts__visual-underline gui-range-time-input__visual-underline" ></div> </div> </div> <span class="gui-range-time-input__separator" aria-hidden="true"> - </span> <div class="gui-parts gui-range-time-input__field" role="group" aria-label="End time" > <!-- The same parts as the start --> </div> </div> </div> </div></gui-range-time>| 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, inside the label, when there is one. |
gui-widget-input, gui-parts-ring | The part that looks like a field. gui-parts-ring draws its focus ring and its error border. |
gui-range-time-input | The field of this element. gui-range-time-input--icon is on it when there is an icon. |
gui-widget-icon | The icon, with your icon classes. |
gui-range-time-input__pills | The pills, a <gui-pills> element. gui-pills__pill is one range. |
gui-pills__count | The count of ranges that replaces the pills when the field is narrower than 540px. |
gui-range-time-input__field | The start, or the end: a gui-parts group of parts. |
gui-range-time-input__time-cluster | The hour and minute, which keep their left-to-right order in a right-to-left page. |
gui-range-time-input__part | One part. gui-range-time-input__dayperiod is the AM/PM button. |
gui-range-time-input__separator | The text between the parts, and between the start and the end (separator, - by default). |
gui-range-time-input__visual-underline | The line under the focused part. |
gui-validator, gui-validator__error | The errors container, when there are errors, and one error message. |
The ids derive from uid, or from a generated id when you don’t set one. Select by class or by
tag, not by id.
Tokens
Section titled “Tokens”The field reads the same design tokens as the Date range
field: --gui-widget-size for its height, --gui-bg-surface, --gui-border-default and
--gui-radius-md for its box, --gui-border-focus, --gui-shadow-focus, --gui-intent-error
and --gui-shadow-focus-error for its states, and --gui-intent-primary with the --gui-pill-*
tokens for its pills. Set them on :root to change every field, or on any element to change the
fields inside it.
/* Rounder fields and pills in the schedule form only. */.schedule { --gui-radius-md: 12px;}To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode.
Accessibility
Section titled “Accessibility”- Name. The
labelnames the field, a group with the start and the end. Without a label the field has no accessible name, and development builds log a warning. - Start and end. The start and the end are groups named “Start time” and “End time”. Change
the names with
start-time-aria-labelandend-time-aria-label; an empty value removes them. - Parts. The hour and minute are spin buttons named “Hour” and “Minute”, with their minimum,
maximum and current value, and the AM/PM part is a button named “AM/PM”. Change the names with
hour-aria-label,minute-aria-labelandday-period-aria-label. - Pills. The pills are a toolbar named “Selected time ranges”. Each pill is a button named by
its range, described as “Remove time”: change that with
remove-pill-aria-label. - Description and errors. The
hintis the field’s description. While errors show, the field hasaria-invalid="true"and points at them witharia-errormessage, and screen readers announce new errors as they appear. - Required.
requiredsetsaria-requiredon every part; the visual marker is hidden from screen readers.
In the parts:
| Key | Action |
|---|---|
| Tab | Moves focus to the first part of the start, and away from the field. The other parts are not tab stops. |
| Digits | Fill the part. A full part moves on to the next empty part, from the start to the end. |
| ← → | Move between the parts, and between the start and the end. ← on the first part moves to the last pill. |
| ↑ ↓ | Increase or decrease the part, or switch AM and PM. |
| Backspace Delete | Clear the part. On an empty part, while every part is empty, they move to the last pill. |
| Enter | Adds the range typed, or saves the range being edited. |
On a pill, the arrows, Home and End move between the pills, and Delete removes the range, as in the Date range field.
What every element shares, such as translating the default names, is in Accessibility.