Skip to content
Components, switch product

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.

Code of the example above
RangeTimeBasic.tsx
import { GuiRangeTimeInput } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function RangeTimeBasic() {
return <GuiRangeTimeInput label="Opening hours" />;
}

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.

HoursField.tsx
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 ?? [])}
/>
);
}

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.

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.

Code of the example above
RangeTimeValue.tsx
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} />;
}
  • 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.

Code of the example above
RangeTimeHourFormat.tsx
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>
);
}

locale-id also sets the format of the pills. See i18n.

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.

Code of the example above
RangeTimeBounds.tsx
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([])}
/>
);
}

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.

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.

Code of the example above
RangeTimeEdit.tsx
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} />
);
}

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.

Code of the example above
RangeTimeDisabledReadonly.tsx
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>
);
}

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.

Code of the example above
RangeTimeNativeForm.tsx
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>
);
}

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.

<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
<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>
ClassPart
gui-fieldThe element itself. Lines its parts up with the fields next to it in a gui-grid row.
gui-label, gui-label__textThe label, and its text with the required marker.
gui-widget-hintThe hint, inside the label, when there is one.
gui-widget-input, gui-parts-ringThe part that looks like a field. gui-parts-ring draws its focus ring and its error border.
gui-range-time-inputThe field of this element. gui-range-time-input--icon is on it when there is an icon.
gui-widget-iconThe icon, with your icon classes.
gui-range-time-input__pillsThe pills, a <gui-pills> element. gui-pills__pill is one range.
gui-pills__countThe count of ranges that replaces the pills when the field is narrower than 540px.
gui-range-time-input__fieldThe start, or the end: a gui-parts group of parts.
gui-range-time-input__time-clusterThe hour and minute, which keep their left-to-right order in a right-to-left page.
gui-range-time-input__partOne part. gui-range-time-input__dayperiod is the AM/PM button.
gui-range-time-input__separatorThe text between the parts, and between the start and the end (separator, - by default).
gui-range-time-input__visual-underlineThe line under the focused part.
gui-validator, gui-validator__errorThe 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.

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.

styles.css
/* 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.

  • Name. The label names 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-label and end-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-label and day-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 hint is the field’s description. While errors show, the field has aria-invalid="true" and points at them with aria-errormessage, and screen readers announce new errors as they appear.
  • Required. required sets aria-required on every part; the visual marker is hidden from screen readers.

In the parts:

KeyAction
TabMoves focus to the first part of the start, and away from the field. The other parts are not tab stops.
DigitsFill 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 DeleteClear the part. On an empty part, while every part is empty, they move to the last pill.
EnterAdds 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.