Skip to content
Components, switch product

Date range picker

<gui-range-date-picker> is a date range field with a calendar in a popup. The user types the start and end dates, or picks them in the calendar, and each range becomes a pill. The field holds as many ranges as the user adds.

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

It combines a Date range field and a Range calendar. For a single date, use the Date picker.

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

TripsField.tsx
import { useState } from 'react';
import { GuiRangeDatePicker } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function TripsField() {
const [trips, setTrips] = useState([]);
return (
<GuiRangeDatePicker
label="Trip dates"
name="trips"
value={trips}
onGuiChange={(event) => setTrips(event.detail.value ?? [])}
/>
);
}

Properties

Property Type Default Description
icon string '' Icon class name shown inside the control, for example from an icon font.
toggleAriaLabel attribute toggle-aria-label string – Accessible name of the button that opens the popup. An empty value keeps the default.
dayAriaLabel attribute day-aria-label string – Accessible name of the day part. An empty value keeps the default.
monthAriaLabel attribute month-aria-label string – Accessible name of the month part. An empty value keeps the default.
yearAriaLabel attribute year-aria-label string – Accessible name of the year part. An empty value keeps the default.
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.
localeId attribute locale-id string – BCP 47 locale for formatting and parsing, such as en-US or es.
value DateRange[] [] The date ranges, as { start, end } ISO dates.
separator string – Text shown between the start and end of a range.
removePillAriaLabel attribute remove-pill-aria-label string – Accessible name of the remove button of each range pill. An empty value keeps the default.
startDateAriaLabel attribute start-date-aria-label string – Accessible name of the start date field. An empty value removes it.
endDateAriaLabel attribute end-date-aria-label string – Accessible name of the end date field. An empty value removes it.
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' – How day numbers are written.
weekdayFormat attribute weekday-format 'short' | 'long' | 'narrow' – How weekday names are written in the header.
monthFormat attribute month-format 'numeric' | '2-digit' | 'long' | 'short' | 'narrow' – 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 – Number of months shown side by side.
invalidDateMessage attribute invalid-date-message string – Error for a complete but impossible date, such as February 31.
minDateMessage attribute min-date-message string – Error for a date before minDate.
maxDateMessage attribute max-date-message string – Error for a date after maxDate.
disabledDateRangeMessage attribute disabled-date-range-message string – Error for a date inside disabledRanges.
incompleteMessage attribute incomplete-message string – Error when focus leaves a partly filled value.
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 added, removed or finished editing a range. detail.value is the list of ranges.
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.

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.

The calendar opens when a part of the field gets focus, or from the button at the end of the field. In the calendar:

  1. The first day the user picks 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.

The new range becomes a pill and the calendar stays open for the next one. The days the user picks also fill the parts of the field, and a range typed in the field shows in the calendar.

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. A range that overlaps or touches another one merges with it, and the pills show in date order.

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

min-date and max-date limit the days the user can pick, as ISO dates. disabled-ranges takes a JSON list of { start, end } ranges the user can’t pick. Out-of-bounds and disabled days show as disabled in the calendar.

Code of the example above
RangeDatePickerBounds.tsx
import { GuiRangeDatePicker } from '@golemui/gui-components/react';
import type { DateRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const closedWeekends: DateRange[] = [
{ start: '2027-03-13', end: '2027-03-14' },
{ start: '2027-03-27', end: '2027-03-28' },
];
const booking: DateRange[] = [{ start: '2027-03-02', end: '2027-03-05' }];
export function RangeDatePickerBounds() {
return (
<GuiRangeDatePicker
label="Studio booking"
hint="March and April only. The studio is closed on the weekends of March 13 and March 27."
minDate="2027-03-01"
maxDate="2027-04-30"
disabledRanges={closedWeekends}
value={booking}
/>
);
}

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. The calendar shows the rejected range in red, the field shows its dates, and no pill is added. A range typed in the field is checked the same way, against the bounds too.

When the picker rejects what the user entered, such as a range across a disabled day or a date like February 31, it fires gui-input-error with the message in event.detail.message. It doesn’t show the message itself: set it as errors, and clear it on the next gui-input, which fires when the user adds a range.

Code of the example above
RangeDatePickerErrors.tsx
import { useState } from 'react';
import { GuiRangeDatePicker } from '@golemui/gui-components/react';
import type { DateRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const booked: DateRange[] = [{ start: '2027-03-10', end: '2027-03-12' }];
export function RangeDatePickerErrors() {
const [value, setValue] = useState<DateRange[] | undefined>([
{ start: '2027-03-01', end: '2027-03-03' },
]);
const [errors, setErrors] = useState<string[]>([]);
return (
<GuiRangeDatePicker
id="cabin-dates"
label="Cabin stay"
hint="The cabin is booked from March 10 to March 12. Pick a range across those days."
disabledRanges={booked}
value={value}
errors={errors}
onGuiInputError={(event) => setErrors([event.detail.message])}
onGuiInput={(event) => {
setValue(event.detail.value);
setErrors([]);
}}
/>
);
}

Change the messages with disabled-date-range-message, min-date-message, max-date-message, invalid-date-message and incomplete-message, or translate them for every element, see i18n.

number-of-months shows several months side by side, which helps with long ranges.

Code of the example above
RangeDatePickerMonths.tsx
import { GuiRangeDatePicker } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function RangeDatePickerMonths() {
return <GuiRangeDatePicker label="Summer rental" numberOfMonths={2} />;
}

locale-id sets the language of the dates: the order of the parts, the format of the pills, the names of the months and weekdays, and the first day of the week. Without it, the picker uses English (en). See i18n.

Code of the example above
RangeDatePickerLocale.tsx
import { GuiRangeDatePicker } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function RangeDatePickerLocale() {
return <GuiRangeDatePicker label="Fechas del viaje" localeId="es-ES" />;
}

day-format, weekday-format and month-format change how the calendar writes the days, the weekday names and the month.

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 calendar, 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
RangeDatePickerEdit.tsx
import { GuiRangeDatePicker } from '@golemui/gui-components/react';
import type { DateRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const offsites: DateRange[] = [
{ start: '2026-03-09', end: '2026-03-11' },
{ start: '2026-06-15', end: '2026-06-17' },
];
export function RangeDatePickerEdit() {
return (
<GuiRangeDatePicker label="Team offsites" allowEdit value={offsites} />
);
}

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

Code of the example above
RangeDatePickerNativeForm.tsx
import { useState } from 'react';
import { GuiButton, GuiRangeDatePicker } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function RangeDatePickerNativeForm() {
const [result, setResult] = useState('');
return (
<form
id="rental-form"
onSubmit={(event) => {
event.preventDefault();
const ranges = new FormData(event.currentTarget).getAll('rental');
setResult(`Submitted ${ranges.join(', ')}`);
}}
>
<div className="gui-grid">
<div className="gui-grid__cell">
<GuiRangeDatePicker label="Rental dates" name="rental" required />
</div>
<div className="gui-grid__cell">
<GuiButton type="submit" label="Book" />
</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 picker gives the user, such as the disabled-date-range-message.

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

<gui-range-date-picker> renders into the light DOM, so you can style every part with plain CSS. It renders a <gui-range-date> field, a toggle button and, while open, a panel with a <gui-range-calendar>:

gui-range-date-picker
<gui-range-date-picker>
<span class="gui-label" id="trip_label">
<span class="gui-label__text">Trip dates</span>
</span>
<div class="gui-widget">
<gui-range-date id="trip_date" class="gui-field">
<!-- The field: see the anatomy on the Date range page -->
</gui-range-date>
<button
type="button"
class="gui-range-date-picker__arrow"
aria-haspopup="dialog"
aria-label="Show calendar"
aria-expanded="true"
aria-controls="trip_popup"
>
<span class="gui-caret" aria-hidden="true"></span>
</button>
<div class="gui-picker__panel">
<gui-range-calendar
role="dialog"
id="trip_popup"
aria-labelledby="trip_label"
class="gui-field"
>
<!-- The calendar: see the anatomy on the Range calendar page -->
</gui-range-calendar>
</div>
</div>
<div
class="gui-validator gui-validator--empty"
id="trip_errors"
role="alert"
></div>
</gui-range-date-picker>
ClassPart
gui-label, gui-label__textThe label, and its text with the required marker.
gui-widgetThe box around the field, the button and the panel.
gui-range-date-picker__arrowThe button that opens and closes the calendar. Its gui-caret turns while the calendar is open.
gui-picker__panelThe popup that holds the calendar. While there are errors, it repeats them at its bottom.
gui-validatorThe errors container. gui-validator--empty is on it while there are no errors.

The field inside is a <gui-range-date> and the calendar a <gui-range-calendar>: their classes style the picker too. 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 tokens of the Date range field, and the calendar those of the Range calendar. The popup reads these:

TokenWhat it styles
--gui-bg-defaultThe background of the popup.
--gui-border-defaultThe border of the popup.
--gui-radius-mdThe corner radius of the popup.
--gui-shadow-mdThe shadow of the popup.
--gui-space-2The padding of the popup.
--gui-intent-errorThe popup’s border while invalid.
--gui-text-defaultThe toggle button’s caret.
styles.css
/* Bigger days in every range picker. */
gui-range-date-picker {
--gui-calendar-day-button-size: 44px;
}

To restyle beyond the tokens, see Overriding styles; for dark mode and the Clay theme, see Themes and dark mode. To change the caret, see Carets.

  • Name. The label names the field and the calendar popup. Without a label, the field has no accessible name and the popup is named “Calendar”.
  • Toggle button. The button that opens the calendar is named “Show calendar”; change it with toggle-aria-label. It has aria-haspopup="dialog", and aria-expanded tells whether the calendar is open.
  • Popup. The calendar is a dialog. Its days are a grid: each day is named by its full date, such as “Monday, March 1, 2027”, today has aria-current="date", the days in a range have aria-selected="true", and disabled days have aria-disabled="true".
  • Month changes. The calendar announces the month and year as the user moves to another month.
  • Start, end and parts. As in the Date range field: the start and end are named “Start date” and “End date”, and each part by its type.
  • Calendar buttons. The month buttons are named “Previous month” and “Next month”, and the year button “Select 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.
KeyAction
TabMoves focus to the field’s first part, then to the toggle button, then into the open calendar. Leaving the picker closes it.
Focus on a partOpens the calendar, and keeps the focus in the field so the user can type.
Enter SpaceOn the toggle button, opens the calendar and moves focus to it, or closes it.
← → ↑ ↓In the calendar, 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 calendarMove 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 calendarMove to the first or last day of the week the user can pick, within the month.
Enter SpaceIn the calendar, picks the focused day: the first pick starts a range, the second completes it.
EscapeCloses the calendar and moves focus back to the field. In the year grid, closes the grid first.

The keys of the field and its pills are on the Date range page. What every element shares, such as translating the default names, is in Accessibility.