Skip to content
Components, switch product

Range calendar

<gui-range-calendar> is a calendar, always open on the page, to pick date ranges. Each range the user picks becomes a pill above the days.

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

It is the calendar the Date range picker opens in its popup. For a single date, use the Calendar.

The element’s entry point is @golemui/gui-components/range-calendar, which registers <gui-range-calendar> 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.

HolidaysField.tsx
import { useState } from 'react';
import { GuiRangeCalendar } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function HolidaysField() {
const [holidays, setHolidays] = useState([]);
return (
<GuiRangeCalendar
label="Holidays"
name="holidays"
value={holidays}
onGuiChange={(event) => setHolidays(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 DateRange[] – Dates that cannot be picked, as { start, end } ISO date ranges.
numberOfMonths attribute number-of-months number 1 Number of months shown side by side.
value DateRange[] [] The date ranges, as { start, end } ISO dates.
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 date inside disabledRanges.
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-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.
  1. The first day the user picks, with a click or Enter, is one end of the range.
  2. As the pointer or the focus moves over other days, the calendar previews the range.
  3. The second day completes the range. It can come before the first one: the range is ordered for you. Picking the same day twice makes a range of one day.

gui-change fires each time the user adds or removes a range, with the whole list in event.detail.value.

value is a list of ranges. Each range is an object with a start and an end, both ISO dates (YYYY-MM-DD); a range of one day has a start only. 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
RangeCalendarValue.tsx
import { GuiRangeCalendar } from '@golemui/gui-components/react';
import type { DateRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const breaks: DateRange[] = [
{ start: '2027-04-05', end: '2027-04-09' },
{ start: '2027-04-19' },
];
export function RangeCalendarValue() {
return <GuiRangeCalendar label="School breaks" value={breaks} />;
}

A range that overlaps or touches another one merges with it: picking April 15 to 22 above joins the one-day range of April 19. The pills show in date order. A pill’s remove button, or Delete on a focused pill, removes its range.

min-date and max-date limit the days the user can pick, as ISO dates, and stop the month buttons at their months. disabled-ranges takes a JSON list of { start, end } ranges the user can’t pick.

Code of the example above
RangeCalendarBounds.tsx
import { useState } from 'react';
import { GuiRangeCalendar } from '@golemui/gui-components/react';
import type { DateRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const soldOut: DateRange[] = [{ start: '2027-06-10', end: '2027-06-11' }];
export function RangeCalendarBounds() {
const [value, setValue] = useState<DateRange[] | undefined>([
{ start: '2027-06-02', end: '2027-06-04' },
]);
const [errors, setErrors] = useState<string[]>([]);
return (
<GuiRangeCalendar
id="festival-days"
label="Festival passes"
hint="The festival runs from June 1 to June 20. June 10 and 11 are sold out."
minDate="2027-06-01"
maxDate="2027-06-20"
disabledRanges={soldOut}
value={value}
errors={errors}
onGuiInputError={(event) => setErrors([event.detail.message])}
onGuiInput={(event) => {
setValue(event.detail.value);
setErrors([]);
}}
/>
);
}

A range can’t step over a disabled day: a range with a disabled day anywhere inside it is rejected, not only one that starts or ends on it. Pick June 8, then June 12, above: the calendar shows the range in red, adds no pill, and fires gui-input-error with the message in event.detail.message. The calendar 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 message with disabled-date-range-message.

number-of-months shows several months side by side, which helps with long ranges. The month buttons move all of them together.

Code of the example above
RangeCalendarMonths.tsx
import { GuiRangeCalendar } from '@golemui/gui-components/react';
import type { DateRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const semester: DateRange[] = [{ start: '2027-09-13', end: '2027-10-08' }];
export function RangeCalendarMonths() {
return (
<GuiRangeCalendar
label="Exchange semester"
numberOfMonths={2}
value={semester}
/>
);
}

locale-id sets the language of the calendar: the names of the months and weekdays, the first day of the week and the format of the pills. Without it, the calendar uses English (en). weekday-format, month-format and day-format change how it writes the weekday names, the month and the days. See i18n.

Code of the example above
RangeCalendarLocale.tsx
import { GuiRangeCalendar } from '@golemui/gui-components/react';
import type { DateRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const holidays: DateRange[] = [{ start: '2027-07-12', end: '2027-07-23' }];
export function RangeCalendarLocale() {
return (
<GuiRangeCalendar
label="Congés"
localeId="fr-FR"
weekdayFormat="short"
value={holidays}
/>
);
}

With a name, <gui-range-calendar> submits one entry per range under that name. Each entry is the range as JSON, such as {"start":"2027-04-05","end":"2027-04-09"}, or {"start":"2027-04-19"} for a single day. Read them all with FormData.getAll(). required blocks the submit while the calendar has no range.

Code of the example above
RangeCalendarNativeForm.tsx
import { useState } from 'react';
import { GuiButton, GuiRangeCalendar } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function RangeCalendarNativeForm() {
const [result, setResult] = useState('');
return (
<form
id="leave-form"
onSubmit={(event) => {
event.preventDefault();
const ranges = new FormData(event.currentTarget).getAll('leave');
setResult(`Submitted ${ranges.join(', ')}`);
}}
>
<div className="gui-grid">
<div className="gui-grid__cell">
<GuiRangeCalendar label="Parental leave" name="leave" required />
</div>
<div className="gui-grid__cell">
<GuiButton type="submit" label="Submit request" />
</div>
</div>
<p role="status">{result}</p>
</form>
);
}

A range that starts before min-date, ends after max-date or steps over a day of 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-date-range-message.

How the elements submit, validate and reset is in Native forms.

<gui-range-calendar> renders into the light DOM, so you can style every part with plain CSS. These are the parts and their public classes, with one range, a hint and an error:

gui-range-calendar
<gui-range-calendar class="gui-field">
<label class="gui-label" id="breaks_calendar_label">
<span class="gui-label__text">School breaks</span>
<div class="gui-widget-hint" id="breaks_hint" aria-hidden="true">
Up to three breaks.
</div>
</label>
<div class="gui-widget">
<div
class="gui-calendar-input"
role="group"
id="breaks_calendar"
aria-labelledby="breaks_calendar_label"
aria-describedby="breaks_hint"
aria-invalid="true"
aria-errormessage="breaks_errors"
>
<div class="gui-visually-hidden" aria-live="polite">April 2027</div>
<gui-pills class="gui-range-calendar__pills">
<div class="gui-pills__strip-wrapper">
<div
class="gui-pills__strip"
role="toolbar"
aria-label="Selected date ranges"
>
<button
type="button"
class="gui-pills__pill gui-pills__pill--clickable"
aria-label="04/05/2027 - 04/09/2027"
aria-description="Remove date"
>
<span class="gui-pills__pill-text">04/05/2027 - 04/09/2027</span>
<span class="gui-pills__pill-remove" aria-hidden="true">
<svg>…</svg>
</span>
</button>
</div>
</div>
</gui-pills>
<div class="gui-calendar__container">
<button
type="button"
class="gui-button gui-calendar__month-button gui-calendar__month-button--prev"
aria-label="Previous month"
>
<span class="gui-caret gui-caret--left" aria-hidden="true"></span>
</button>
<div class="gui-calendar__months-grid">
<div class="gui-calendar__panel">
<header class="gui-calendar__header">
<h2>
<span class="gui-calendar__month-name">April</span>
<button
type="button"
class="gui-calendar__year-selector"
aria-expanded="false"
aria-label="Select year, 2027"
>
<span class="gui-calendar__year-value">2027</span>
<span class="gui-calendar__year-arrow" aria-hidden="true">
<span class="gui-caret" aria-hidden="true"></span>
</span>
</button>
</h2>
</header>
<div
class="gui-calendar__days-grid"
role="grid"
aria-label="April 2027"
>
<div role="row" class="gui-calendar__rows">
<span class="gui-calendar__weekday" role="columnheader">S</span>
<!-- The other weekdays -->
</div>
<div role="row" class="gui-calendar__rows">
<button
type="button"
role="gridcell"
class="gui-calendar__day-button other-month"
disabled
aria-label="Sunday, March 28, 2027"
aria-selected="false"
>
28
</button>
<!-- The other days of the week -->
</div>
<div role="row" class="gui-calendar__rows">
<button
type="button"
role="gridcell"
class="gui-calendar__day-button range-start"
tabindex="0"
aria-label="Monday, April 5, 2027"
aria-selected="true"
>
5
</button>
<button
type="button"
role="gridcell"
class="gui-calendar__day-button in-range"
tabindex="-1"
aria-label="Tuesday, April 6, 2027"
aria-selected="true"
>
6
</button>
<!-- The other days, then the other weeks -->
</div>
</div>
</div>
</div>
<button
type="button"
class="gui-button gui-calendar__month-button gui-calendar__month-button--next"
aria-label="Next month"
>
<span class="gui-caret gui-caret--right" aria-hidden="true"></span>
</button>
</div>
</div>
</div>
<div class="gui-validator" id="breaks_errors" role="alert">
<div class="gui-validator__error">Pick at least one week.</div>
</div>
</gui-range-calendar>
ClassPart
gui-fieldThe element itself.
gui-label, gui-label__textThe label, and its text with the required marker.
gui-widget-hintThe hint.
gui-calendar-inputThe calendar’s box.
gui-range-calendar__pillsThe pills, a <gui-pills> element. There are none while the value is empty.
gui-pills__pillOne range. gui-pills__pill-text is its label and gui-pills__pill-remove its remove icon.
gui-calendar__month-buttonThe month buttons, with --prev or --next.
gui-calendar__months-gridThe months, one gui-calendar__panel each.
gui-calendar__headerA month’s header: gui-calendar__month-name and the gui-calendar__year-selector button.
gui-calendar__days-gridA month’s days. gui-calendar__weekday is a weekday name.
gui-calendar__day-buttonOne day.
range-start, range-end, in-rangeOn the days of a range: its first day, its last day and the days between.
selectedOn a range of one day.
is-anchor, is-selectingWhile picking: the first day picked, and the days of the preview.
invalid-range-start, invalid-range-end, invalid-in-range, invalid-range-singleOn the days of a rejected range.
today, other-month, disabledToday, the days of the next and previous months, and the days the user can’t pick.
gui-calendar__year-gridThe grid of years that replaces the days while the year button is open; gui-calendar__year-button is one year.
gui-validator, gui-validator__errorThe errors container and one error message.

The ids derive from uid, or from a generated id such as gui-range-calendar-3 when you don’t set one. Select by class or by tag, not by id.

The calendar reads these design tokens. Set them on :root to change every calendar, or on any element to change the calendars inside it:

TokenWhat it styles
--gui-calendar-day-button-sizeThe size of each day.
--gui-calendar-change-month-button-width, --gui-calendar-change-month-button-heightThe size of the month buttons.
--gui-calendar-year-button-width, --gui-calendar-year-button-heightThe size of each year in the year grid.
--gui-calendar-year-grid-heightThe height of the year grid.
--gui-bg-defaultThe calendar’s background.
--gui-border-defaultThe border of the calendar and of the year grid.
--gui-radius-mdThe corner radius of the calendar, the days and the pills.
--gui-space-3The padding of the calendar.
--gui-text-defaultThe days and the label.
--gui-text-secondaryThe background of a hovered day.
--gui-intent-primaryThe first and last day of a range, a one-day range, the pills.
--gui-color-whiteThe text on those days and in the pills.
--gui-intent-primary-activeThe border of today, and the current year in the year grid.
--gui-intent-errorThe ends of a rejected range, and the border when there are errors.
--gui-bg-disabledThe days the user can’t pick.
--gui-border-focus, --gui-shadow-focusThe focus ring of the days, the buttons and the pills.
--gui-pill-height, --gui-pill-font-sizeThe height and the text size of the pills.

Two properties of the element itself color the days between the ends of a range. The calendar sets them on its own tag, so set yours on the tag, or on a class of it, not on :root:

PropertyWhat it styles
--gui-range-bgThe days inside a range. Half-transparent --gui-intent-primary by default.
--gui-range-error-bgThe days inside a rejected range. Half-transparent --gui-intent-error by default.
styles.css
/* Bigger days, and a softer range. */
gui-range-calendar {
--gui-calendar-day-button-size: 44px;
--gui-range-bg: color-mix(
in srgb,
var(--gui-intent-primary),
transparent 75%
);
}

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 and the months.
  • Days. Each month’s days are a grid. Each day is named by its full date, such as “Monday, April 5, 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".
  • Month changes. The calendar announces the month and year as the user moves to another month.
  • Buttons. The month buttons are named “Previous month” and “Next month”, and the year button “Select year” with the year. Change them with prev-month-aria-label, next-month-aria-label and select-year-aria-label, and the year grid’s name with year-grid-aria-label.
  • Pills. The pills are a toolbar named “Selected date ranges”. Each pill is a button named by its range, described as “Remove date”: change that with remove-pill-aria-label.
  • Description and errors. The hint is the calendar’s description. While errors show, the calendar has aria-invalid="true" and points at them with aria-errormessage, and screen readers announce new errors as they appear.
KeyAction
TabMoves focus through the pills, the previous-month button, the year button, the days and the next-month button. Today and the first day of each range are tab stops, or the first day the user can pick when the months show neither.
← →Move to the previous or next day, into the next or previous month at the edges, skipping disabled days.
↑ ↓Move to the same day of the previous or next week.
Page Up Page DownMove 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 EndMove 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 a range, the second completes it.
Delete BackspaceOn a pill, remove its range.
Enter Space, in the year gridPick the focused year. The arrows move by one year, or by a row of four.
Escape, in the year gridCloses the year grid and moves focus back to the year button.

What every element shares, such as translating the default names, is in Accessibility.