Skip to content
Components, switch product

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.

Code of the example above
RangeDateTimeCalendarBasic.tsx
import { GuiRangeDateTimeCalendar } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function RangeDateTimeCalendarBasic() {
return <GuiRangeDateTimeCalendar label="Shifts" minuteStep={30} />;
}

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.

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

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.

A range needs four pieces, which the user can pick in any order:

  1. 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.
  2. 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-step sets 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.

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.

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

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.

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

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.

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.

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.

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

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.

<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
<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>
ClassPart
gui-range-date-time-calendar__time-rowThe row of the two times. is-list-open is on it while a list is open.
gui-range-date-time-calendar__time-columnThe 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, __endThe start and end time pickers, two <gui-time-picker> elements.
gui-range-date-time-calendar__badgesThe count bubbles of a day.
gui-range-date-time-calendar__day-countThe bubble with the number of ranges on a day that has more than one.
gui-range-date-time-calendar__disabled-countThe bubble with the number of disabled ranges on a day that has some.
gui-range-date-time-calendar__badge-tooltipThe 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.

The calendar reads the tokens of the Range calendar, including --gui-range-bg and --gui-range-error-bg, and these:

TokenWhat it styles
--gui-calendar-time-grid-heightThe height of an open time list.
--gui-calendar-time-button-heightThe height of each time in the list.
--gui-intent-primaryThe background of the range count.
--gui-color-whiteThe text of the range count.
--gui-bg-disabledThe background of the disabled count.
--gui-radius-fullThe round shape of the counts.
--gui-bg-surfaceThe background of the list a count shows on hover.
--gui-border-defaultThe border of that list.
--gui-shadow-mdThe shadow of that list.
--gui-space-2The space between the two times, and below them.
styles.css
/* 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.

  • Name. The label names 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-label and end-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 have aria-selected="true", and the days the user can’t pick have aria-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-label and disabled-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-label and year-grid-aria-label change the names of the month and year buttons, and remove-pill-aria-label the description of the pills, “Remove date-time” by default.
KeyAction
TabMoves 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 daysMove 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 daysMove to the first or last day of the week the user can pick, within the month.
Enter SpacePick the focused day: the first pick starts the span, the second completes it.
Enter Space, in a time listPick the focused time. The arrows move through the times, four to a row.
EscapeCloses an open time list or the year grid.
Delete BackspaceOn 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.