Date-time range calendar
<gui-range-date-time-calendar> is a calendar, always open on the page, to pick ranges between two
moments: the days in the calendar, and a start and an end time above them. Each range the user
picks becomes a pill above the calendar.
import { GuiRangeDateTimeCalendar } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function RangeDateTimeCalendarBasic() { return <GuiRangeDateTimeCalendar label="Shifts" minuteStep={30} />;}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/range-date-time-calendar';
@Component({ selector: 'app-range-date-time-calendar-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-date-time-calendar label="Shifts" [minuteStep]="30" ></gui-range-date-time-calendar> `,})export class RangeDateTimeCalendarBasicComponent {}<script setup lang="ts">import '@golemui/gui-components/range-date-time-calendar';import '@golemui/gui-components/index.css';</script>
<template> <gui-range-date-time-calendar label="Shifts" :minute-step="30" ></gui-range-date-time-calendar></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/range-date-time-calendar';import '@golemui/gui-components/index.css';
@customElement('range-date-time-calendar-basic')export class RangeDateTimeCalendarBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-date-time-calendar label="Shifts" minute-step="30" ></gui-range-date-time-calendar> `; }}<gui-range-date-time-calendar label="Shifts" minute-step="30"></gui-range-date-time-calendar>It is the popup of the Date-time range picker. For date ranges without times, use the Range calendar.
The element’s entry point is @golemui/gui-components/range-date-time-calendar, which registers
<gui-range-date-time-calendar> and the elements 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 { GuiRangeDateTimeCalendar } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function ShiftsField() { const [shifts, setShifts] = useState([]);
return ( <GuiRangeDateTimeCalendar label="Shifts" name="shifts" value={shifts} onGuiChange={(event) => setShifts(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, GuiRangeDateTimeCalendarEvents } from '@golemui/gui-components';import '@golemui/gui-components/range-date-time-calendar';
type ChangeEvent = GuiEventMap<typeof GuiRangeDateTimeCalendarEvents>['gui-change'];
@Component({ selector: 'app-shifts-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-date-time-calendar label="Shifts" name="shifts" [value]="shifts" (gui-change)="onChange($event)" ></gui-range-date-time-calendar> `,})export class ShiftsFieldComponent { shifts = [];
onChange(event: Event) { this.shifts = (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-date-time-calendar';import '@golemui/gui-components/index.css';
const shifts = ref([]);</script>
<template> <gui-range-date-time-calendar label="Shifts" name="shifts" :value="shifts" @gui-change="shifts = $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-date-time-calendar';import '@golemui/gui-components/index.css';
@customElement('shifts-field')export class ShiftsField extends LitElement { @state() private shifts = [];
protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-date-time-calendar label="Shifts" name="shifts" .value=${this.shifts} @gui-change=${(event: CustomEvent) => (this.shifts = event.detail.value)} ></gui-range-date-time-calendar> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-range-date-time-calendar label="Shifts" name="shifts"></gui-range-date-time-calendar>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/range-date-time-calendar';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-range-date-time-calendar');
element.addEventListener('gui-change', (event) => { console.log(event.detail.value);});Properties
| Property | Type | Default | Description |
|---|---|---|---|
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. |
localeId
attribute locale-id | string | – | BCP 47 locale for formatting and parsing, such as en-US or es. |
prevMonthIcon
attribute prev-month-icon | string | '' | Icon class name of the previous-month button. |
nextMonthIcon
attribute next-month-icon | string | '' | Icon class name of the next-month button. |
prevMonthAriaLabel
attribute prev-month-aria-label | string | – | Accessible name of the previous-month button. An empty value keeps the default. |
nextMonthAriaLabel
attribute next-month-aria-label | string | – | Accessible name of the next-month button. An empty value keeps the default. |
selectYearAriaLabel
attribute select-year-aria-label | string | – | Accessible name of the button that opens the year grid. An empty value keeps the default. |
yearGridAriaLabel
attribute year-grid-aria-label | string | – | Accessible name of the year grid. An empty value keeps the default. |
dayFormat
attribute day-format | 'numeric' | '2-digit' | 'numeric' | How day numbers are written. |
weekdayFormat
attribute weekday-format | 'short' | 'long' | 'narrow' | 'narrow' | How weekday names are written in the header. |
monthFormat
attribute month-format | 'numeric' | '2-digit' | 'long' | 'short' | 'narrow' | 'long' | How the month is written in the header. |
minDate
attribute min-date | string | – | Earliest selectable date, as an ISO date (YYYY-MM-DD). |
maxDate
attribute max-date | string | – | Latest selectable date, as an ISO date (YYYY-MM-DD). |
disabledRanges
attribute disabled-ranges | DateTimeRange[] | – | Date-times that cannot be picked, as { start, end } ISO date-time ranges. |
numberOfMonths
attribute number-of-months | number | 1 | Number of months shown side by side. |
value | DateTimeRange[] | [] | The date-time ranges, as { start, end } ISO date-times. |
removePillAriaLabel
attribute remove-pill-aria-label | string | – | Accessible name of the remove button of each range pill. An empty value keeps the default. |
disabledDateRangeMessage
attribute disabled-date-range-message | string | – | Error for a range that steps over a disabled day. Defaults to disabledRangeMessage. |
hourFormat
attribute hour-format | HourFormat | – | 12- or 24-hour clock. Defaults to the locale's. |
minuteStep
attribute minute-step | number | – | Minutes between the times offered in the list. |
allowCustomTime
attribute allow-custom-time | boolean | false | Allows typing any time, not only picking one from the list. |
startTimeLabel
attribute start-time-label | string | – | Label of the start time. An empty value keeps the default. |
endTimeLabel
attribute end-time-label | string | – | Label of the end time. An empty value keeps the default. |
minDateTime
attribute min-date-time | string | – | Earliest allowed date-time, as an ISO date-time (YYYY-MM-DDTHH:mm:ss). |
maxDateTime
attribute max-date-time | string | – | Latest allowed date-time, as an ISO date-time (YYYY-MM-DDTHH:mm:ss). |
minDateTimeMessage
attribute min-date-time-message | string | – | Error for a date-time before minDateTime. |
maxDateTimeMessage
attribute max-date-time-message | string | – | Error for a date-time after maxDateTime. |
disabledRangeMessage
attribute disabled-range-message | string | – | Error for a time inside disabledRanges. |
noAvailableTimesMessage
attribute no-available-times-message | string | – | Text shown when no time can be picked. |
incompleteMessage
attribute incomplete-message | string | – | Error when focus leaves a partly filled value. |
dayCountAriaLabel
attribute day-count-aria-label | string | – | Read with a day that has several ranges, before the ranges themselves. {count} is the number of ranges. |
disabledDayCountAriaLabel
attribute disabled-day-count-aria-label | string | – | Read with a day that has disabled times, before the times themselves. {count} is the number of disabled ranges. |
allowEdit
attribute allow-edit | boolean | false | Lets the user edit a range in place from its pill. |
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 committed the value. detail.value is the committed value. |
gui-blur | Focus left the control. |
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. |
CSS custom properties
| Property | Default | Description |
|---|---|---|
--gui-calendar-width | – | Width of one month. |
--gui-calendar-day-button-size | – | Size of each day. |
--gui-calendar-change-month-button-width | – | Width of the previous- and next-month buttons. |
--gui-calendar-change-month-button-height | – | Height of the previous- and next-month buttons. |
--gui-calendar-year-button-width | – | Width of each year in the year grid. |
--gui-calendar-year-button-height | – | Height of each year in the year grid. |
--gui-calendar-year-grid-height | – | Height of the year grid. |
--gui-calendar-time-grid-height | – | Height of the time grid. |
--gui-calendar-time-button-height | – | Height of each time in the grid. |
--gui-range-bg | – | Background of the days inside a selected range. |
--gui-range-error-bg | – | Background of the days inside a rejected range. |
--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”Picking a range
Section titled “Picking a range”A range needs four pieces, which the user can pick in any order:
- The days. The first day picked is one end of the span, and the second day completes it, before or after the first. As the pointer or the focus moves over the days, the calendar previews the span. Picking the same day twice makes a span of one day.
- The start and end times. “Start time” and “End time” each open a list of times, which takes
the place of the days while it is open.
minute-stepsets the minutes between the times. On a span of one day, the end list only offers times after the start.
With the four pieces, the range becomes a pill and the times clear for the next one. If focus
leaves the calendar with a range half picked, the calendar fires gui-input-error with the
“incomplete” message (incomplete-message).
By default the times are picked from the lists. allow-custom-time lets the user type any time.
start-time-label and end-time-label change the labels of the two times, and hour-format sets
a 12 or 24-hour clock.
The value
Section titled “The value”value is a list of ranges. Each range is an object with a start and an end, both local ISO
date-times (YYYY-MM-DDTHH:mm:ss), with no time zone. As an attribute, value is a JSON array.
The calendar opens on the month of the first range, or on the current month when it has none.
import { GuiRangeDateTimeCalendar } from '@golemui/gui-components/react';import type { DateTimeRange } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const bookings: DateTimeRange[] = [ { start: '2027-02-08T09:00:00', end: '2027-02-08T11:00:00' }, { start: '2027-02-08T15:00:00', end: '2027-02-08T16:30:00' }, { start: '2027-02-10T13:00:00', end: '2027-02-11T12:00:00' },];
export function RangeDateTimeCalendarValue() { return ( <GuiRangeDateTimeCalendar label="Room bookings" minuteStep={30} value={bookings} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { DateTimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-date-time-calendar';
@Component({ selector: 'app-range-date-time-calendar-value', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-date-time-calendar label="Room bookings" [minuteStep]="30" [value]="bookings" ></gui-range-date-time-calendar> `,})export class RangeDateTimeCalendarValueComponent { bookings: DateTimeRange[] = [ { start: '2027-02-08T09:00:00', end: '2027-02-08T11:00:00' }, { start: '2027-02-08T15:00:00', end: '2027-02-08T16:30:00' }, { start: '2027-02-10T13:00:00', end: '2027-02-11T12:00:00' }, ];}<script setup lang="ts">import type { DateTimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-date-time-calendar';import '@golemui/gui-components/index.css';
const value: DateTimeRange[] = [ { start: '2027-02-08T09:00:00', end: '2027-02-08T11:00:00' }, { start: '2027-02-08T15:00:00', end: '2027-02-08T16:30:00' }, { start: '2027-02-10T13:00:00', end: '2027-02-11T12:00:00' },];</script>
<template> <gui-range-date-time-calendar label="Room bookings" :minute-step="30" :value.prop="value" ></gui-range-date-time-calendar></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { DateTimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-date-time-calendar';import '@golemui/gui-components/index.css';
const bookings: DateTimeRange[] = [ { start: '2027-02-08T09:00:00', end: '2027-02-08T11:00:00' }, { start: '2027-02-08T15:00:00', end: '2027-02-08T16:30:00' }, { start: '2027-02-10T13:00:00', end: '2027-02-11T12:00:00' },];
@customElement('range-date-time-calendar-value')export class RangeDateTimeCalendarValue extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-date-time-calendar label="Room bookings" minute-step="30" .value=${bookings} ></gui-range-date-time-calendar> `; }}<gui-range-date-time-calendar label="Room bookings" minute-step="30" value='[{"start":"2027-02-08T09:00:00","end":"2027-02-08T11:00:00"},{"start":"2027-02-08T15:00:00","end":"2027-02-08T16:30:00"},{"start":"2027-02-10T13:00:00","end":"2027-02-11T12:00:00"}]'></gui-range-date-time-calendar>A day with more than one range shows their count: hover it to see the ranges. A range that overlaps or touches another one merges with it, and the pills show in time order. A pill’s remove button, or Delete on a focused pill, removes its range.
gui-change fires each time the user adds or removes a range, with the whole list in
event.detail.value.
Bounds and disabled times
Section titled “Bounds and disabled times”min-date-time and max-date-time limit both ends of a range, as ISO date-times: the days before
and after them are disabled, and on their own days the time lists start or stop at their time.
min-date and max-date limit the days only.
disabled-ranges takes a JSON list of { start, end } date-time ranges the user can’t book:
- A day the disabled ranges cover whole is disabled.
- A day they cover in part shows a count of its disabled ranges; hover the count to see their times. On that day, the time lists disable those times.
- A range that overlaps a disabled range is rejected, and so is a span of days that steps over a disabled day.
import { useState } from 'react';import { GuiRangeDateTimeCalendar } from '@golemui/gui-components/react';import type { DateTimeRange } from '@golemui/gui-components';import '@golemui/gui-components/index.css';
const closed: DateTimeRange[] = [ { start: '2027-02-20T00:00:00', end: '2027-02-21T23:59:59' }, { start: '2027-02-15T12:00:00', end: '2027-02-15T13:00:00' }, { start: '2027-02-16T12:00:00', end: '2027-02-16T13:00:00' },];
export function RangeDateTimeCalendarDisabled() { const [value, setValue] = useState<DateTimeRange[] | undefined>([ { start: '2027-02-02T09:00:00', end: '2027-02-02T17:00:00' }, ]); const [errors, setErrors] = useState<string[]>([]);
return ( <GuiRangeDateTimeCalendar id="lab-time" label="Lab time" hint="The lab is closed on February 20 and 21, and cleaned every day from 12:00 PM to 1:00 PM on February 15 and 16." minuteStep={60} minDateTime="2027-02-01T07:00:00" maxDateTime="2027-02-28T19:00:00" disabledRanges={closed} value={value} errors={errors} onGuiInputError={(event) => setErrors([event.detail.message])} onGuiInput={(event) => { setValue(event.detail.value); setErrors([]); }} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { DateTimeRange, GuiEventMap, GuiRangeDateTimeCalendarEvents,} from '@golemui/gui-components';import '@golemui/gui-components/range-date-time-calendar';
type RangeDateTimeCalendarEvent< K extends keyof typeof GuiRangeDateTimeCalendarEvents,> = GuiEventMap<typeof GuiRangeDateTimeCalendarEvents>[K];
@Component({ selector: 'app-range-date-time-calendar-disabled', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-date-time-calendar id="lab-time" label="Lab time" hint="The lab is closed on February 20 and 21, and cleaned every day from 12:00 PM to 1:00 PM on February 15 and 16." [minuteStep]="60" min-date-time="2027-02-01T07:00:00" max-date-time="2027-02-28T19:00:00" [disabledRanges]="closed" [value]="booking" [errors]="errors()" (gui-input-error)="onInputError($event)" (gui-input)="errors.set([])" ></gui-range-date-time-calendar> `,})export class RangeDateTimeCalendarDisabledComponent { closed: DateTimeRange[] = [ { start: '2027-02-20T00:00:00', end: '2027-02-21T23:59:59' }, { start: '2027-02-15T12:00:00', end: '2027-02-15T13:00:00' }, { start: '2027-02-16T12:00:00', end: '2027-02-16T13:00:00' }, ];
booking: DateTimeRange[] = [ { start: '2027-02-02T09:00:00', end: '2027-02-02T17:00:00' }, ];
errors = signal<string[]>([]);
onInputError(event: Event) { this.errors.set([ (event as RangeDateTimeCalendarEvent<'gui-input-error'>).detail.message, ]); }}<script setup lang="ts">import { ref } from 'vue';import type { DateTimeRange } from '@golemui/gui-components';import '@golemui/gui-components/range-date-time-calendar';import '@golemui/gui-components/index.css';
const disabledRanges: DateTimeRange[] = [ { start: '2027-02-20T00:00:00', end: '2027-02-21T23:59:59' }, { start: '2027-02-15T12:00:00', end: '2027-02-15T13:00:00' }, { start: '2027-02-16T12:00:00', end: '2027-02-16T13:00:00' },];const value: DateTimeRange[] = [ { start: '2027-02-02T09:00:00', end: '2027-02-02T17:00:00' },];const errors = ref<string[]>([]);</script>
<template> <gui-range-date-time-calendar id="lab-time" label="Lab time" hint="The lab is closed on February 20 and 21, and cleaned every day from 12:00 PM to 1:00 PM on February 15 and 16." :minute-step="60" min-date-time="2027-02-01T07:00:00" max-date-time="2027-02-28T19:00:00" :disabledRanges.prop="disabledRanges" :value.prop="value" :errors.prop="errors" @gui-input-error="errors = [$event.detail.message]" @gui-input="errors = []" ></gui-range-date-time-calendar></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { DateTimeRange, GuiEventMap, GuiRangeDateTimeCalendarEvents,} from '@golemui/gui-components';import '@golemui/gui-components/range-date-time-calendar';import '@golemui/gui-components/index.css';
type RangeDateTimeCalendarEvent< K extends keyof typeof GuiRangeDateTimeCalendarEvents,> = GuiEventMap<typeof GuiRangeDateTimeCalendarEvents>[K];
const closures: DateTimeRange[] = [ { start: '2027-02-20T00:00:00', end: '2027-02-21T23:59:59' }, { start: '2027-02-15T12:00:00', end: '2027-02-15T13:00:00' }, { start: '2027-02-16T12:00:00', end: '2027-02-16T13:00:00' },];
const labTime: DateTimeRange[] = [ { start: '2027-02-02T09:00:00', end: '2027-02-02T17:00:00' },];
@customElement('range-date-time-calendar-disabled')export class RangeDateTimeCalendarDisabled extends LitElement { @state() private errors: string[] = [];
protected override createRenderRoot() { return this; }
private onInputError(event: RangeDateTimeCalendarEvent<'gui-input-error'>) { this.errors = [event.detail.message]; }
private onInput() { this.errors = []; }
override render() { return html` <gui-range-date-time-calendar id="lab-time" label="Lab time" hint="The lab is closed on February 20 and 21, and cleaned every day from 12:00 PM to 1:00 PM on February 15 and 16." minute-step="60" min-date-time="2027-02-01T07:00:00" max-date-time="2027-02-28T19:00:00" .disabledRanges=${closures} .value=${labTime} .errors=${this.errors} @gui-input-error=${this.onInputError} @gui-input=${this.onInput} ></gui-range-date-time-calendar> `; }}<gui-range-date-time-calendar id="lab-time" label="Lab time" hint="The lab is closed on February 20 and 21, and cleaned every day from 12:00 PM to 1:00 PM on February 15 and 16." minute-step="60" min-date-time="2027-02-01T07:00:00" max-date-time="2027-02-28T19:00:00" disabled-ranges='[{"start":"2027-02-20T00:00:00","end":"2027-02-21T23:59:59"},{"start":"2027-02-15T12:00:00","end":"2027-02-15T13:00:00"},{"start":"2027-02-16T12:00:00","end":"2027-02-16T13:00:00"}]' value='[{"start":"2027-02-02T09:00:00","end":"2027-02-02T17:00:00"}]'></gui-range-date-time-calendar>
<script type="module"> const lab = document.querySelector('#lab-time');
lab.addEventListener('gui-input-error', (event) => { lab.errors = [event.detail.message]; }); lab.addEventListener('gui-input', () => { lab.errors = []; });</script>When the calendar 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 when the user adds a range. Change the messages with
disabled-range-message, disabled-date-range-message (for a span over a disabled day),
min-date-time-message and max-date-time-message.
Calendar options
Section titled “Calendar options”number-of-months shows several months side by side. day-format, weekday-format and
month-format change how the calendar writes the days, the weekday names and the month, and
locale-id sets its language, its clock and the format of the pills. See
i18n.
In a form
Section titled “In a form”With a name, <gui-range-date-time-calendar> submits one entry per range under that name. Each
entry is the range as JSON, such as {"start":"2026-10-12T09:00:00","end":"2026-10-14T17:30:00"}.
Read them all with FormData.getAll(). required blocks the submit while the calendar has no
range.
import { useState } from 'react';import { GuiButton, GuiRangeDateTimeCalendar,} from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function RangeDateTimeCalendarNativeForm() { const [result, setResult] = useState('');
return ( <form id="studio-form" onSubmit={(event) => { event.preventDefault(); const ranges = new FormData(event.currentTarget).getAll('sessions'); setResult(`Submitted ${ranges.join(', ')}`); }} > <div className="gui-grid"> <div className="gui-grid__cell"> <GuiRangeDateTimeCalendar label="Recording sessions" name="sessions" minuteStep={60} required /> </div> <div className="gui-grid__cell"> <GuiButton type="submit" label="Book sessions" /> </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-date-time-calendar';
@Component({ selector: 'app-range-date-time-calendar-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="studio-form" #form (submit)="onSubmit($event, form)"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-date-time-calendar label="Recording sessions" name="sessions" [minuteStep]="60" required ></gui-range-date-time-calendar> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Book sessions"></gui-button> </div> </div> <p id="studio-result" role="status">{{ result() }}</p> </form> `,})export class RangeDateTimeCalendarNativeFormComponent { result = signal('');
onSubmit(event: Event, form: HTMLFormElement) { event.preventDefault(); const ranges = new FormData(form).getAll('sessions'); 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-date-time-calendar';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('sessions'); result.value = `Submitted ${ranges.join(', ')}`;}</script>
<template> <form id="studio-form" ref="form" @submit.prevent="onSubmit"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-date-time-calendar label="Recording sessions" name="sessions" :minute-step="60" :required="true" ></gui-range-date-time-calendar> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Book sessions"></gui-button> </div> </div> <p id="studio-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-date-time-calendar';import '@golemui/gui-components/index.css';
@customElement('range-date-time-calendar-native-form')export class RangeDateTimeCalendarNativeForm 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('sessions'); }
override render() { return html` <form id="studio-form" @submit=${this.onSubmit}> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-date-time-calendar label="Recording sessions" name="sessions" minute-step="60" required ></gui-range-date-time-calendar> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Book sessions"></gui-button> </div> </div> <p role="status"> ${this.ranges && `Submitted ${this.ranges.join(', ')}`} </p> </form> `; }}<form id="studio-form"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-date-time-calendar label="Recording sessions" name="sessions" minute-step="60" required ></gui-range-date-time-calendar> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Book sessions"></gui-button> </div> </div> <p id="studio-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#studio-form');
form.addEventListener('submit', (event) => { event.preventDefault(); const ranges = new FormData(form).getAll('sessions'); document.querySelector('#studio-result').textContent = `Submitted ${ranges.join(', ')}`; });</script>A range that starts before min-date-time or min-date, ends after max-date-time or
max-date, or overlaps disabled-ranges blocks the submit too, even one you set from code. The
browser shows the same message the calendar 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-date-time-calendar> renders into the light DOM, so you can style every part with plain
CSS. It has the parts of the Range calendar,
with a row of two time pickers below the month’s header and count bubbles on the days:
<gui-range-date-time-calendar class="gui-field"> <label class="gui-label" id="rooms_calendar_label"> <span class="gui-label__text">Room bookings</span> </label> <div class="gui-widget"> <div class="gui-calendar-input" role="group" id="rooms_calendar" aria-labelledby="rooms_calendar_label" > <div class="gui-visually-hidden" aria-live="polite">February 2027</div> <gui-pills class="gui-range-calendar__pills"> <!-- One pill per range, as in gui-range-calendar --> </gui-pills> <div class="gui-calendar__container"> <!-- The month buttons, as in gui-range-calendar --> <div class="gui-calendar__months-grid"> <div class="gui-calendar__panel"> <header class="gui-calendar__header"> <!-- The month name and the year button --> </header> <div class="gui-range-date-time-calendar__time-row"> <div class="gui-range-date-time-calendar__time-column"> <gui-time-picker class="gui-time-picker gui-field gui-range-date-time-calendar__start" > <!-- A time picker labeled "Start time" --> </gui-time-picker> </div> <div class="gui-range-date-time-calendar__time-column"> <gui-time-picker class="gui-time-picker gui-field gui-range-date-time-calendar__end" > <!-- A time picker labeled "End time" --> </gui-time-picker> </div> </div> <div class="gui-calendar__days-grid" role="grid" aria-label="February 2027" > <!-- The weekday names, then a row per week --> <button type="button" role="gridcell" class="gui-calendar__day-button selected" tabindex="0" aria-label="Monday, February 8, 2027" aria-description="2 ranges: 02/08/2027, 09:00 AM - 02/08/2027, 11:00 AM, 02/08/2027, 03:00 PM - 02/08/2027, 04:30 PM" aria-selected="true" > 8 <span class="gui-range-date-time-calendar__badges"> <span aria-hidden="true" class="gui-range-date-time-calendar__day-count" > 2 </span> <span class="gui-range-date-time-calendar__badge-tooltip" aria-hidden="true" > <span>02/08/2027, 09:00 AM - 02/08/2027, 11:00 AM</span> <span>02/08/2027, 03:00 PM - 02/08/2027, 04:30 PM</span> </span> </span> </button> <!-- The other days --> </div> </div> </div> </div> </div> </div></gui-range-date-time-calendar>| Class | Part |
|---|---|
gui-range-date-time-calendar__time-row | The row of the two times. is-list-open is on it while a list is open. |
gui-range-date-time-calendar__time-column | The column of one time. is-open is on it while its list is open, and the other one hides. |
gui-range-date-time-calendar__start, __end | The start and end time pickers, two <gui-time-picker> elements. |
gui-range-date-time-calendar__badges | The count bubbles of a day. |
gui-range-date-time-calendar__day-count | The bubble with the number of ranges on a day that has more than one. |
gui-range-date-time-calendar__disabled-count | The bubble with the number of disabled ranges on a day that has some. |
gui-range-date-time-calendar__badge-tooltip | The list of ranges or disabled times that shows while the pointer is over a bubble. |
The calendar, the days and the pills have the classes of the
Range calendar, and the time pickers those of
the Time picker. 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 calendar reads the tokens of the Range calendar,
including --gui-range-bg and --gui-range-error-bg, and these:
| Token | What it styles |
|---|---|
--gui-calendar-time-grid-height | The height of an open time list. |
--gui-calendar-time-button-height | The height of each time in the list. |
--gui-intent-primary | The background of the range count. |
--gui-color-white | The text of the range count. |
--gui-bg-disabled | The background of the disabled count. |
--gui-radius-full | The round shape of the counts. |
--gui-bg-surface | The background of the list a count shows on hover. |
--gui-border-default | The border of that list. |
--gui-shadow-md | The shadow of that list. |
--gui-space-2 | The space between the two times, and below them. |
/* Shorter time lists, with smaller times. */gui-range-date-time-calendar { --gui-calendar-time-grid-height: 160px; --gui-calendar-time-button-height: 28px;}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 calendar, a group with the pills, the times and the months. - Times. The start and end times are time pickers labeled “Start time” and “End time”; change
the labels with
start-time-labelandend-time-label. Each opens its list from a button named “Show time list”. - Days. Each month’s days are a grid. Each day is named by its full date, such as
“Monday, February 8, 2027”. Today has
aria-current="date", the days in a range havearia-selected="true", and the days the user can’t pick havearia-disabled="true". - Counts. The count bubbles are hidden from screen readers. Their content is the day’s
description instead, such as “2 ranges” or “1 disabled range” followed by the ranges. Change the
counts with
day-count-aria-labelanddisabled-day-count-aria-label, where{count}is the number. - Month changes. The calendar announces the month and year as the user moves to another month.
- Buttons and pills. As in the Range calendar:
prev-month-aria-label,next-month-aria-label,select-year-aria-labelandyear-grid-aria-labelchange the names of the month and year buttons, andremove-pill-aria-labelthe description of the pills, “Remove date-time” by default.
| Key | Action |
|---|---|
| Tab | Moves focus through the pills, the previous-month button, the year button, the two times, the days and the next-month button. |
| ← → ↑ ↓ | In the days, move by a day or a week, into the next or previous month at the edges, skipping disabled days. |
| Page Up Page Down, in the days | Move to the same day in the previous or next month (the last day of a shorter month), or the nearest day the user can pick. With Shift, the same day in the previous or next year. |
| Home End, in the days | Move to the first or last day of the week the user can pick, within the month. |
| Enter Space | Pick the focused day: the first pick starts the span, the second completes it. |
| Enter Space, in a time list | Pick the focused time. The arrows move through the times, four to a row. |
| Escape | Closes an open time list or the year grid. |
| Delete Backspace | On a pill, remove its range. |
The keys of the time pickers are on the Time picker page. What every element shares, such as translating the default names, is in Accessibility.