Skip to content
Components, switch product

Date range

<gui-range-date> is a field to type date ranges. The user types a start and an end date part by part, presses Enter, and the range becomes a pill. The field holds as many ranges as the user adds.

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

To pick the days in a calendar instead, use the Date range picker. For a single date, use the Date input.

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

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

Properties

Property Type Default Description
localeId attribute locale-id string – BCP 47 locale for formatting and parsing, such as en-US or es.
showErrors attribute show-errors boolean true Whether the element renders its own error list. Elements that embed it turn it off and show the errors themselves.
showHint attribute show-hint boolean true Whether the element renders its hint. Elements that embed it turn it off and show the hint themselves: aria-describedby still points at the hint by its id.
icon string '' Icon class name shown inside the control, for example from an icon font.
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.
value DateRange[] [] The date ranges, as { start, end } ISO dates.
invalidDateMessage attribute invalid-date-message string – Error for a complete but impossible date, such as February 31.
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.
separator string – Text shown between the start and end of a range.
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-focus One of the parts of the field received focus.
gui-input-error The element rejected what the user entered, such as an impossible date or a value out of bounds. detail.message is the error; show it through errors.
gui-parts-change The typed parts changed before they form a complete value, for a host that mirrors them.
gui-edit-state-change Editing a range in place started, changed selection or ended. detail has the selected range and whether it is editing.
gui-range-click The user clicked a range pill. detail.range is the range.
gui-dropdown-toggle The count of ranges opened or closed its dropdown. detail.open is the new state.

CSS custom properties

Property Default Description
--gui-pill-height – Height of each pill.
--gui-pill-font-size – Font size of the pill text.
--gui-pill-action-size – Size of the icons inside a pill.
--gui-pill-action-hit – Clickable area of the buttons inside a pill.

value is a list of ranges. Each range is an object with a start and an end, both ISO dates (YYYY-MM-DD). A range of one day has a start only. As an attribute, value is a JSON array.

Code of the example above
RangeDateValue.tsx
import { GuiRangeDateInput } from '@golemui/gui-components/react';
import type { DateRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const closures: DateRange[] = [
{ start: '2026-12-24', end: '2026-12-26' },
{ start: '2026-12-31' },
];
export function RangeDateValue() {
return <GuiRangeDateInput label="Office closures" value={closures} />;
}

The field keeps the list tidy as the user adds ranges:

  • An end typed before its start is swapped, so 10/09/2026 - 10/05/2026 becomes October 5 to 9.
  • A range that overlaps or touches another one merges with it: October 5 to 9 and October 10 to 12 become October 5 to 12.
  • 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.

locale-id sets the language of the dates: the order of the parts, such as day, month and year in Spanish, and the format of the pills. See i18n.

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

allow-edit lets the user change a range they already added. Select a pill, then press F2 or E, or use its edit button: the range loads into the parts and the pill shows the change as the user types. Enter or the confirm button saves it, and Escape or the cancel button keeps the old range. Screen readers announce each step.

Code of the example above
RangeDateEdit.tsx
import { GuiRangeDateInput } 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 RangeDateEdit() {
return <GuiRangeDateInput label="Team offsites" allowEdit value={offsites} />;
}

A disabled field can’t be focused or changed, and a form doesn’t submit it. A readonly field shows its ranges but doesn’t let the user add or remove one, and a form submits its value.

Code of the example above
RangeDateDisabledReadonly.tsx
import { GuiRangeDateInput } from '@golemui/gui-components/react';
import type { DateRange } from '@golemui/gui-components';
import '@golemui/gui-components/index.css';
const august: DateRange[] = [{ start: '2026-08-01', end: '2026-08-15' }];
export function RangeDateDisabledReadonly() {
return (
<div className="gui-grid">
<div className="gui-grid__cell">
<GuiRangeDateInput label="Blackout dates" value={august} disabled />
</div>
<div className="gui-grid__cell">
<GuiRangeDateInput label="Booked stays" value={august} readOnly />
</div>
</div>
);
}

The field checks each date as the user enters it. When it rejects one, such as February 31, or when focus leaves a range typed halfway, 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 once the problem is fixed.

Code of the example above
RangeDateErrors.tsx
import { useState } from 'react';
import { GuiRangeDateInput } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function RangeDateErrors() {
const [errors, setErrors] = useState<string[]>([]);
return (
<GuiRangeDateInput
id="leave-dates"
label="Leave dates"
hint="Type a date that doesn't exist, such as 02/31/2026, then leave the field."
errors={errors}
onGuiInputError={(event) => setErrors([event.detail.message])}
onGuiInput={() => setErrors([])}
/>
);
}

Your own validation works the same way: set errors to a list of messages. See Errors.

With a name, <gui-range-date> submits one entry per range under that name. Each entry is the range as JSON, such as {"start":"2026-10-05","end":"2026-10-09"}, or {"start":"2026-12-28"} 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
RangeDateNativeForm.tsx
import { useState } from 'react';
import { GuiButton, GuiRangeDateInput } from '@golemui/gui-components/react';
import '@golemui/gui-components/index.css';
export function RangeDateNativeForm() {
const [result, setResult] = useState('');
return (
<form
id="vacation-form"
onSubmit={(event) => {
event.preventDefault();
const ranges = new FormData(event.currentTarget).getAll('vacation');
setResult(`Submitted ${ranges.join(', ')}`);
}}
>
<div className="gui-grid">
<div className="gui-grid__cell">
<GuiRangeDateInput label="Vacation" name="vacation" required />
</div>
<div className="gui-grid__cell">
<GuiButton type="submit" label="Request" />
</div>
</div>
<p role="status">{result}</p>
</form>
);
}

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

<gui-range-date> 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, an icon and an error:

gui-range-date
<gui-range-date class="gui-field">
<span class="gui-label" id="trip_label">
<span class="gui-label__text">
Trip dates<span aria-hidden="true"> *</span>
</span>
<div class="gui-widget-hint" id="trip_hint" aria-hidden="true">
Add every leg of the trip.
</div>
</span>
<div class="gui-widget">
<div
role="group"
class="gui-widget-input gui-parts-ring gui-range-date-input gui-range-date-input--icon"
aria-labelledby="trip_label"
aria-describedby="trip_hint"
aria-invalid="true"
aria-errormessage="trip_errors"
>
<span
aria-hidden="true"
class="gui-widget-icon icon-calendar"
data-icon="icon-calendar"
></span>
<gui-pills class="gui-range-date-input__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="10/05/2026 - 10/09/2026"
aria-description="Remove date"
>
<span class="gui-pills__pill-text">10/05/2026 - 10/09/2026</span>
<span class="gui-pills__pill-remove" aria-hidden="true">
<svg>…</svg>
</span>
</button>
</div>
</div>
<div class="gui-pills__compact">
<button
type="button"
class="gui-pills__count"
aria-haspopup="true"
aria-label="1 date range"
>
1
</button>
</div>
</gui-pills>
<div class="gui-range-date-input__inputs">
<div
class="gui-parts gui-range-date-input__field"
role="group"
aria-label="Start date"
>
<div
class="gui-parts__touch-target gui-range-date-input__touch-target"
>
<input
type="text"
inputmode="numeric"
role="spinbutton"
class="gui-parts__part gui-range-date-input__part"
placeholder="mm"
aria-label="Month"
aria-required="true"
/>
<div
class="gui-parts__visual-underline gui-range-date-input__visual-underline"
></div>
</div>
<span class="gui-range-date-input__separator">/</span>
<!-- The day, then the year, with gui-parts__year gui-range-date-input__year -->
</div>
<span class="gui-range-date-input__separator" aria-hidden="true">
-
</span>
<div
class="gui-parts gui-range-date-input__field"
role="group"
aria-label="End date"
>
<!-- The same parts as the start -->
</div>
</div>
</div>
</div>
<div class="gui-validator" id="trip_errors" role="alert">
<div class="gui-validator__error">Add the return trip.</div>
</div>
</gui-range-date>
ClassPart
gui-fieldThe element itself. Lines its parts up with the fields next to it in a gui-grid row.
gui-label, gui-label__textThe label, and its text with the required marker.
gui-widget-hintThe hint.
gui-widgetThe box around the control.
gui-widget-input, gui-parts-ringThe part that looks like a field. gui-parts-ring draws its focus ring and its error border.
gui-range-date-inputThe field of this element, with the pills and the parts.
gui-range-date-input--iconOn the field when there is an icon: it makes room for it.
gui-widget-iconThe icon, with your icon classes.
gui-range-date-input__pillsThe pills, a <gui-pills> element.
gui-pills__pillOne range. gui-pills__pill-text is its label and gui-pills__pill-remove its remove icon.
gui-pills__countThe count of ranges that replaces the pills when the field is narrow. It opens them in a dropdown.
gui-range-date-input__inputsThe start and the end.
gui-range-date-input__fieldThe start, or the end: a gui-parts group of parts.
gui-range-date-input__partOne part, such as the month. gui-parts__part is the class every segmented field shares.
gui-range-date-input__yearThe year part, which is wider.
gui-range-date-input__separatorThe text between the parts, and between the start and the end (separator, - by default).
gui-range-date-input__visual-underlineThe line under the focused part.
gui-validator, gui-validator__errorThe errors container, when there are errors, and one error message.

The pills switch to the count when the field is narrower than 540px. The ids derive from uid, or from a generated id such as gui-range-date-3 when you don’t set one. Select by class or by tag, not by id.

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

TokenWhat it styles
--gui-widget-sizeThe height of the field.
--gui-radius-mdThe corner radius of the field and the pills.
--gui-bg-surfaceThe field’s background.
--gui-border-defaultThe border, and the lines after the icon and the pills.
--gui-text-defaultThe parts, the label and the icon.
--gui-font-familyThe font of the pills. The parts use a monospace font.
--gui-font-smThe text size of the label and the field.
--gui-font-xsThe text size of the hint, the errors and the count.
--gui-text-secondaryThe hint.
--gui-border-focusThe border while a part or a pill has focus.
--gui-shadow-focusThe focus ring of the field and of the pills.
--gui-intent-errorThe border and the messages when there are errors.
--gui-shadow-focus-errorThe focus ring when there are errors.
--gui-bg-disabledThe background when disabled.
--gui-text-disabledThe text when disabled.
--gui-intent-primaryThe background of the pills and the count.
--gui-color-whiteThe text of the pills and the count.
--gui-pill-heightThe height of each pill.
--gui-pill-font-sizeThe text size of the pills.
--gui-pill-action-size, --gui-pill-action-hitThe size of the icons in a pill, and of their clickable area.
--gui-bg-default, --gui-shadow-mdThe background and the shadow of the dropdown the count opens.
--gui-space-1, --gui-space-2The space between the label, hint and errors, and between pills and parts.
styles.css
/* Taller pills with larger text. */
gui-range-date {
--gui-pill-height: 28px;
--gui-pill-font-size: var(--gui-font-sm);
}

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

  • Name. The label names the field, a group with the start and the end. Without a label the field has no accessible name, and development builds log a warning.
  • Start and end. The start and the end are groups named “Start date” and “End date”. Change the names with start-date-aria-label and end-date-aria-label; an empty value removes them.
  • Parts. Each part is a spin button named “Day”, “Month” or “Year”, with its minimum, maximum and current value. Change the names with day-aria-label, month-aria-label and year-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. When the field is narrow, the count button is named after the number of ranges, such as “2 date ranges”.
  • Description and errors. The hint is the field’s description. While errors show, the field has aria-invalid="true" and points at them with aria-errormessage, and screen readers announce new errors as they appear.
  • Required. required sets aria-required on every part; the visual marker is hidden from screen readers.
  • Editing. With allow-edit, screen readers announce when editing starts, is saved or is cancelled. Change the announcements with edit-started-message, edit-committed-message and edit-cancelled-message.

In the parts:

KeyAction
TabMoves focus to the first part of the start, and away from the field. The other parts are not tab stops.
DigitsFill the part. A full part moves on to the next empty part, from the start to the end.
← →Move between the parts, and between the start and the end. ← on the first part moves to the last pill.
↑ ↓Increase or decrease the part.
Backspace DeleteClear the part. On an empty part, while every part is empty, they move to the last pill.
EnterAdds the range typed, or saves the range being edited.

On a pill:

KeyAction
← →Move to the previous or next pill. → on the last pill moves back to the parts.
Home EndMove to the first or last pill.
Delete BackspaceRemove the range.
F2 EWith allow-edit, edit the range.
EscapeWith allow-edit, cancels the edit, then clears the selected pill.

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