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.
import { GuiRangeCalendar } from '@golemui/gui-components/react';import '@golemui/gui-components/index.css';
export function RangeCalendarBasic() { return <GuiRangeCalendar label="Holidays" />;}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import '@golemui/gui-components/range-calendar';
@Component({ selector: 'app-range-calendar-basic', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: `<gui-range-calendar label="Holidays"></gui-range-calendar>`,})export class RangeCalendarBasicComponent {}<script setup lang="ts">import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';</script>
<template> <gui-range-calendar label="Holidays"></gui-range-calendar></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';
@customElement('range-calendar-basic')export class RangeCalendarBasic extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html`<gui-range-calendar label="Holidays"></gui-range-calendar>`; }}<gui-range-calendar label="Holidays"></gui-range-calendar>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.
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 ?? [])} /> );}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, GuiRangeCalendarEvents } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';
type ChangeEvent = GuiEventMap<typeof GuiRangeCalendarEvents>['gui-change'];
@Component({ selector: 'app-holidays-field', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-calendar label="Holidays" name="holidays" [value]="holidays" (gui-change)="onChange($event)" ></gui-range-calendar> `,})export class HolidaysFieldComponent { holidays = [];
onChange(event: Event) { this.holidays = (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-calendar';import '@golemui/gui-components/index.css';
const holidays = ref([]);</script>
<template> <gui-range-calendar label="Holidays" name="holidays" :value="holidays" @gui-change="holidays = $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-calendar';import '@golemui/gui-components/index.css';
@customElement('holidays-field')export class HolidaysField extends LitElement { @state() private holidays = [];
protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-calendar label="Holidays" name="holidays" .value=${this.holidays} @gui-change=${(event: CustomEvent) => (this.holidays = event.detail.value)} ></gui-range-calendar> `; }}With any bundler, such as Vite. Import the element once, anywhere, and use the tag in your markup.
<gui-range-calendar label="Holidays" name="holidays"></gui-range-calendar>
<script type="module" src="./main.js"></script>import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';
const element = document.querySelector('gui-range-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 | 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. |
Examples
Section titled “Examples”Picking a range
Section titled “Picking a range”- The first day the user picks, with a click or Enter, is one end of the range.
- As the pointer or the focus moves over other days, the calendar previews the range.
- 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.
The value
Section titled “The 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.
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} />;}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { DateRange } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';
@Component({ selector: 'app-range-calendar-value', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-calendar label="School breaks" [value]="breaks" ></gui-range-calendar> `,})export class RangeCalendarValueComponent { breaks: DateRange[] = [ { start: '2027-04-05', end: '2027-04-09' }, { start: '2027-04-19' }, ];}<script setup lang="ts">import type { DateRange } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';
const value: DateRange[] = [ { start: '2027-04-05', end: '2027-04-09' }, { start: '2027-04-19' },];</script>
<template> <gui-range-calendar label="School breaks" :value.prop="value" ></gui-range-calendar></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { DateRange } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';
const schoolBreaks: DateRange[] = [ { start: '2027-04-05', end: '2027-04-09' }, { start: '2027-04-19' },];
@customElement('range-calendar-value')export class RangeCalendarValue extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-calendar label="School breaks" .value=${schoolBreaks} ></gui-range-calendar> `; }}<gui-range-calendar label="School breaks" value='[{"start":"2027-04-05","end":"2027-04-09"},{"start":"2027-04-19"}]'></gui-range-calendar>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.
Bounds and disabled days
Section titled “Bounds and disabled days”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.
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([]); }} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import type { DateRange, GuiEventMap, GuiRangeCalendarEvents,} from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';
type RangeCalendarEvent<K extends keyof typeof GuiRangeCalendarEvents> = GuiEventMap<typeof GuiRangeCalendarEvents>[K];
@Component({ selector: 'app-range-calendar-bounds', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-calendar id="festival-days" label="Festival passes" hint="The festival runs from June 1 to June 20. June 10 and 11 are sold out." min-date="2027-06-01" max-date="2027-06-20" [disabledRanges]="soldOut" [value]="passes" [errors]="errors()" (gui-input-error)="onInputError($event)" (gui-input)="errors.set([])" ></gui-range-calendar> `,})export class RangeCalendarBoundsComponent { soldOut: DateRange[] = [{ start: '2027-06-10', end: '2027-06-11' }]; passes: DateRange[] = [{ start: '2027-06-02', end: '2027-06-04' }];
errors = signal<string[]>([]);
onInputError(event: Event) { this.errors.set([ (event as RangeCalendarEvent<'gui-input-error'>).detail.message, ]); }}<script setup lang="ts">import { ref } from 'vue';import type { DateRange } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';
const disabledRanges: DateRange[] = [ { start: '2027-06-10', end: '2027-06-11' },];const value: DateRange[] = [{ start: '2027-06-02', end: '2027-06-04' }];const errors = ref<string[]>([]);</script>
<template> <gui-range-calendar id="festival-days" label="Festival passes" hint="The festival runs from June 1 to June 20. June 10 and 11 are sold out." min-date="2027-06-01" max-date="2027-06-20" :disabledRanges.prop="disabledRanges" :value.prop="value" :errors.prop="errors" @gui-input-error="errors = [$event.detail.message]" @gui-input="errors = []" ></gui-range-calendar></template>import { LitElement, html } from 'lit';import { customElement, state } from 'lit/decorators.js';import type { DateRange, GuiEventMap, GuiRangeCalendarEvents,} from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';
type RangeCalendarEvent<K extends keyof typeof GuiRangeCalendarEvents> = GuiEventMap<typeof GuiRangeCalendarEvents>[K];
const soldOut: DateRange[] = [{ start: '2027-06-10', end: '2027-06-11' }];
const passes: DateRange[] = [{ start: '2027-06-02', end: '2027-06-04' }];
@customElement('range-calendar-bounds')export class RangeCalendarBounds extends LitElement { @state() private errors: string[] = [];
protected override createRenderRoot() { return this; }
private onInputError(event: RangeCalendarEvent<'gui-input-error'>) { this.errors = [event.detail.message]; }
private onInput() { this.errors = []; }
override render() { return html` <gui-range-calendar id="festival-days" label="Festival passes" hint="The festival runs from June 1 to June 20. June 10 and 11 are sold out." min-date="2027-06-01" max-date="2027-06-20" .disabledRanges=${soldOut} .value=${passes} .errors=${this.errors} @gui-input-error=${this.onInputError} @gui-input=${this.onInput} ></gui-range-calendar> `; }}<gui-range-calendar id="festival-days" label="Festival passes" hint="The festival runs from June 1 to June 20. June 10 and 11 are sold out." min-date="2027-06-01" max-date="2027-06-20" disabled-ranges='[{"start":"2027-06-10","end":"2027-06-11"}]' value='[{"start":"2027-06-02","end":"2027-06-04"}]'></gui-range-calendar>
<script type="module"> const festival = document.querySelector('#festival-days');
festival.addEventListener('gui-input-error', (event) => { festival.errors = [event.detail.message]; }); festival.addEventListener('gui-input', () => { festival.errors = []; });</script>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.
Several months
Section titled “Several months”number-of-months shows several months side by side, which helps with long ranges. The month
buttons move all of them together.
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} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { DateRange } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';
@Component({ selector: 'app-range-calendar-months', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-calendar label="Exchange semester" [numberOfMonths]="2" [value]="semester" ></gui-range-calendar> `,})export class RangeCalendarMonthsComponent { semester: DateRange[] = [{ start: '2027-09-13', end: '2027-10-08' }];}<script setup lang="ts">import type { DateRange } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';
const value: DateRange[] = [{ start: '2027-09-13', end: '2027-10-08' }];</script>
<template> <gui-range-calendar label="Exchange semester" :number-of-months="2" :value.prop="value" ></gui-range-calendar></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { DateRange } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';
const semester: DateRange[] = [{ start: '2027-09-13', end: '2027-10-08' }];
@customElement('range-calendar-months')export class RangeCalendarMonths extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-calendar label="Exchange semester" number-of-months="2" .value=${semester} ></gui-range-calendar> `; }}<gui-range-calendar label="Exchange semester" number-of-months="2" value='[{"start":"2027-09-13","end":"2027-10-08"}]'></gui-range-calendar>Locale and formats
Section titled “Locale and formats”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.
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} /> );}import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';import type { DateRange } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';
@Component({ selector: 'app-range-calendar-locale', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <gui-range-calendar label="Congés" locale-id="fr-FR" weekday-format="short" [value]="leave" ></gui-range-calendar> `,})export class RangeCalendarLocaleComponent { leave: DateRange[] = [{ start: '2027-07-12', end: '2027-07-23' }];}<script setup lang="ts">import type { DateRange } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';
const value: DateRange[] = [{ start: '2027-07-12', end: '2027-07-23' }];</script>
<template> <gui-range-calendar label="Congés" locale-id="fr-FR" weekday-format="short" :value.prop="value" ></gui-range-calendar></template>import { LitElement, html } from 'lit';import { customElement } from 'lit/decorators.js';import type { DateRange } from '@golemui/gui-components';import '@golemui/gui-components/range-calendar';import '@golemui/gui-components/index.css';
const holidays: DateRange[] = [{ start: '2027-07-12', end: '2027-07-23' }];
@customElement('range-calendar-locale')export class RangeCalendarLocale extends LitElement { protected override createRenderRoot() { return this; }
override render() { return html` <gui-range-calendar label="Congés" locale-id="fr-FR" weekday-format="short" .value=${holidays} ></gui-range-calendar> `; }}<gui-range-calendar label="Congés" locale-id="fr-FR" weekday-format="short" value='[{"start":"2027-07-12","end":"2027-07-23"}]'></gui-range-calendar>In a form
Section titled “In a form”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.
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> );}import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core';import '@golemui/gui-components/button';import '@golemui/gui-components/range-calendar';
@Component({ selector: 'app-range-calendar-native-form', schemas: [CUSTOM_ELEMENTS_SCHEMA], template: ` <form id="leave-form" #form (submit)="onSubmit($event, form)"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-calendar label="Parental leave" name="leave" required ></gui-range-calendar> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Submit request"></gui-button> </div> </div> <p id="leave-result" role="status">{{ result() }}</p> </form> `,})export class RangeCalendarNativeFormComponent { result = signal('');
onSubmit(event: Event, form: HTMLFormElement) { event.preventDefault(); const ranges = new FormData(form).getAll('leave'); 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-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('leave'); result.value = `Submitted ${ranges.join(', ')}`;}</script>
<template> <form id="leave-form" ref="form" @submit.prevent="onSubmit"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-calendar label="Parental leave" name="leave" :required="true" ></gui-range-calendar> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Submit request"></gui-button> </div> </div> <p id="leave-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-calendar';import '@golemui/gui-components/index.css';
@customElement('range-calendar-native-form')export class RangeCalendarNativeForm 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('leave'); }
override render() { return html` <form id="leave-form" @submit=${this.onSubmit}> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-calendar label="Parental leave" name="leave" required ></gui-range-calendar> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Submit request"></gui-button> </div> </div> <p role="status"> ${this.ranges && `Submitted ${this.ranges.join(', ')}`} </p> </form> `; }}<form id="leave-form"> <div class="gui-grid"> <div class="gui-grid__cell"> <gui-range-calendar label="Parental leave" name="leave" required></gui-range-calendar> </div> <div class="gui-grid__cell"> <gui-button type="submit" label="Submit request"></gui-button> </div> </div> <p id="leave-result" role="status"></p></form>
<script type="module"> const form = document.querySelector('#leave-form');
form.addEventListener('submit', (event) => { event.preventDefault(); const ranges = new FormData(form).getAll('leave'); document.querySelector('#leave-result').textContent = `Submitted ${ranges.join(', ')}`; });</script>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.
Styling
Section titled “Styling”Anatomy
Section titled “Anatomy”<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 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>| Class | Part |
|---|---|
gui-field | The element itself. |
gui-label, gui-label__text | The label, and its text with the required marker. |
gui-widget-hint | The hint. |
gui-calendar-input | The calendar’s box. |
gui-range-calendar__pills | The pills, a <gui-pills> element. There are none while the value is empty. |
gui-pills__pill | One range. gui-pills__pill-text is its label and gui-pills__pill-remove its remove icon. |
gui-calendar__month-button | The month buttons, with --prev or --next. |
gui-calendar__months-grid | The months, one gui-calendar__panel each. |
gui-calendar__header | A month’s header: gui-calendar__month-name and the gui-calendar__year-selector button. |
gui-calendar__days-grid | A month’s days. gui-calendar__weekday is a weekday name. |
gui-calendar__day-button | One day. |
range-start, range-end, in-range | On the days of a range: its first day, its last day and the days between. |
selected | On a range of one day. |
is-anchor, is-selecting | While picking: the first day picked, and the days of the preview. |
invalid-range-start, invalid-range-end, invalid-in-range, invalid-range-single | On the days of a rejected range. |
today, other-month, disabled | Today, the days of the next and previous months, and the days the user can’t pick. |
gui-calendar__year-grid | The grid of years that replaces the days while the year button is open; gui-calendar__year-button is one year. |
gui-validator, gui-validator__error | The 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.
Tokens
Section titled “Tokens”The calendar reads these design tokens. Set them on :root to change every calendar, or on any
element to change the calendars inside it:
| Token | What it styles |
|---|---|
--gui-calendar-day-button-size | The size of each day. |
--gui-calendar-change-month-button-width, --gui-calendar-change-month-button-height | The size of the month buttons. |
--gui-calendar-year-button-width, --gui-calendar-year-button-height | The size of each year in the year grid. |
--gui-calendar-year-grid-height | The height of the year grid. |
--gui-bg-default | The calendar’s background. |
--gui-border-default | The border of the calendar and of the year grid. |
--gui-radius-md | The corner radius of the calendar, the days and the pills. |
--gui-space-3 | The padding of the calendar. |
--gui-text-default | The days and the label. |
--gui-text-secondary | The background of a hovered day. |
--gui-intent-primary | The first and last day of a range, a one-day range, the pills. |
--gui-color-white | The text on those days and in the pills. |
--gui-intent-primary-active | The border of today, and the current year in the year grid. |
--gui-intent-error | The ends of a rejected range, and the border when there are errors. |
--gui-bg-disabled | The days the user can’t pick. |
--gui-border-focus, --gui-shadow-focus | The focus ring of the days, the buttons and the pills. |
--gui-pill-height, --gui-pill-font-size | The 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:
| Property | What it styles |
|---|---|
--gui-range-bg | The days inside a range. Half-transparent --gui-intent-primary by default. |
--gui-range-error-bg | The days inside a rejected range. Half-transparent --gui-intent-error by default. |
/* 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.
Accessibility
Section titled “Accessibility”- Name. The
labelnames 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 havearia-selected="true", and the days the user can’t pick havearia-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-labelandselect-year-aria-label, and the year grid’s name withyear-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
hintis the calendar’s description. While errors show, the calendar hasaria-invalid="true"and points at them witharia-errormessage, and screen readers announce new errors as they appear.
| Key | Action |
|---|---|
| Tab | Moves 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 Down | 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 | 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 a range, the second completes it. |
| Delete Backspace | On a pill, remove its range. |
| Enter Space, in the year grid | Pick the focused year. The arrows move by one year, or by a row of four. |
| Escape, in the year grid | Closes the year grid and moves focus back to the year button. |
What every element shares, such as translating the default names, is in Accessibility.