Values are YYYY-MM-DD strings with no time zone, so a selected 20th never becomes the 19th. The Calendar has no surface of its own; place it in a Card, Popover or page section that provides one. Day and navigation controls are actual Button components; caption selectors are actual Native Select components. Add a border or fill on the host, or style the calendar part.
Range
selection-mode="range" starts a range on the first activation and completes it on the second. As an attribute, a range is written from/to. visible-months shows consecutive months side by side. A range with only from is incomplete; its anchor is both range start and range end.
Range
Default density
Compact density
Restart from the initial state
Show the copyable source
minimum-nights and maximum-nights limit the range length; range-exclusion decides whether a range across a disabled date is rejected or restarted.
Multiple dates
In multiple mode each activation toggles one date. Write the attribute value as space-separated dates, and cap the selection with maximum-selection-count.
Multiple
Default density
Compact density
Restart from the initial state
Show the copyable source
min and max disable dates outside the selection window while still displaying them. For specific dates, assign a Set or predicate to the disabledDates or unavailableDates property.
Bookable dates
Default density
Compact density
Restart from the initial state
Show the copyable source
<tp-card section-colors="off" style="inline-size:fit-content">
<tp-calendar
id="calendar-bookable"
label="Appointment"
today="2026-06-08"
min="2026-06-08"
max="2026-06-26"
></tp-calendar>
</tp-card>
<script type="module">
const calendar = document.getElementById('calendar-bookable');
await customElements.whenDefined('tp-calendar');
// Weekends are closed; two days are already fully booked.
const booked = new Set(['2026-06-11', '2026-06-18']);
calendar.unavailableDates = (date) => booked.has(date);
calendar.disabledDates = (date) => [0, 6].includes(new Date(date + 'T00:00').getDay());
</script>
Disabled and unavailable dates are skipped by keyboard movement and cannot be selected; unavailable ones carry a separate data-unavailable marker for styling.
Custom day content
Nightly prices and booked dates
Default density
Compact density
Restart from the initial state
Show the copyable source
renderDay puts your own content inside each day button, here a nightly rate, while the button keeps its date name, focus and selection. Keep that content passive. dayModifiers tags dates with a data-booked marker and day.modifiers.booked, so the struck-through days can also say “booked” to screen readers through messages.day. --tp-calendar-cell-size makes room for the extra line.
The day Button is the only activation and focus target, so custom content cannot add interactive descendants: links, buttons, form controls, library controls, tabindex or editable elements, and interactive ARIA roles are unsupported. Such content produces a calendar-interactive-day-content diagnostic. Modifier names must be lowercase (a-z, 0-9, -).
--tp-calendar-cell-size sets the size of every day, weekday, week-number and navigation cell. Increase it for custom day content, responsively if needed:
caption-layout="dropdown" replaces the caption text with native selects, which helps when the date may be years away, such as a birth date. Use dropdown-months or dropdown-years for one selector.
Without navigation bounds, the year dropdown offers the current year minus 100 through the current year. That list does not restrict selection. With navigation bounds, a month choice is disabled when that month cannot be displayed, and a year choice is disabled when none of its months can be; the displayed year always stays available. Choosing a year keeps the displayed month when it can be shown; if that month of the chosen year is outside the bounds, the change lands on the nearest month of that year that can be shown. Either way it is a normal tp-displayed-month-change proposal.
Dropdown caption
Default density
Compact density
Restart from the initial state
Show the copyable source
show-week-number adds a locale-aware week column, fixed-weeks keeps six rows so the height never jumps, and the footer slot adds content below the months. Empty footers take no space. Custom adapters must implement weekNumber.
Week numbers
Default density
Compact density
Restart from the initial state
Show the copyable source
Shortcut buttons can drive a controlled Calendar: set value to pick the date and displayed-month to bring its month into view. Because both are controlled, the listeners accept the reader’s own clicks and month changes too. fixed-weeks keeps the card from changing height between months.
Date and time
Date with start and end times
Default density
Compact density
Restart from the initial state
Show the copyable source
Calendar values are dates only. Collect times in labelled time inputs beside it and combine the two in your own code.
Locale
locale takes a BCP 47 tag and formats the caption, weekday names, day numbers and accessible date names through Intl. Without it the Calendar follows the document language, then the browser. The first day of the week also comes from the locale, Monday in French and Sunday in Brazilian Portuguese, unless week-starts-on sets it.
French and Brazilian Portuguese
Default density
Compact density
Restart from the initial state
Show the copyable source
The value stays a YYYY-MM-DD string in every locale. label names the group; the remaining control names and validation messages come from the messages property, which falls back to English. The French calendar above names its navigation buttons that way.
month is the first day of the month the control navigates to, and caption its locale-formatted name. CalendarValidityCode is value-missing, excluded-date, selection-count, range-excluded-date or range-nights; the message is reported through checkValidity(), reportValidity() and Field. Diagnostics from tp-diagnostic are developer-facing and stay in English.
calendar.locale = 'fr';
calendar.label = 'Date de livraison';
calendar.messages = {
previous: (_month, caption) => `Mois précédent, ${caption}`,
next: (_month, caption) => `Mois suivant, ${caption}`,
monthDropdown: 'Mois',
yearDropdown: 'Année',
weekNumberHeader: 'Numéro de semaine',
validity: (code) =>
code === 'value-missing' ? 'Choisissez une date.' : 'Choisissez une date disponible.',
};
Arabic, right to left
Default density
Compact density
Restart from the initial state
Show the copyable source
Inside dir="rtl" the grid, the navigation chevrons, the arrow keys and the rounded ends of a range all flip. The locale only formats the text; dates stay Gregorian YYYY-MM-DD values.
Date picker
In a popover
Default density
Compact density
Restart from the initial state
Show the copyable source
A Popover supplies the floating surface and returns focus to the trigger; the Calendar supplies the date. Close the popover once a date is picked. A tp-time label formats the chosen date for the reader’s locale and shows its own text, “Pick a date”, until there is one.
Forms and events
With a name, the Calendar submits its value and takes part in validation; required reports a missing date through Field. Each selection emits a cancelable tp-value-change whose detail.value holds the new selection. The form value is the single date, one repeated entry per date in multiple mode, or from and to entries for a range. Reset restores defaultValue. Required reports valueMissing (message messages.validity('value-missing')) with no selection and prevents clearing a non-empty selection. Read-only keeps navigation and focus but rejects selection. Disabled removes the calendar from focus and submission.
Value change
Default density
Compact density
Restart from the initial state
Show the copyable source
For a controlled Calendar, set value and assign the proposed value synchronously in the tp-value-change listener; leaving it unchanged rejects the change. Canceling the event rejects it in either mode. Programmatic and controlled values are validated against the same bounds, exclusions, counts and night limits as activation.
Keyboard
One day is in the tab order. Arrow keys move by day and week, Home and End to the week’s edges, Page Up and Page Down by month, and Shift with Page Up or Page Down by year. Enter and Space select the focused day. Movement skips disabled and hidden dates and reveals other months within the navigation bounds. Consumed keys and keys carrying Alt, Ctrl or Meta are left to the browser.
API Reference
Every element also accepts motion-policy.
Attributes
AttributePropertyTypeDefaultDescriptionselection-modeselectionModeCalendarSelectionMode'single'Whether a click selects a single date, toggles one of several dates, or starts and completes a
range.minminstring''Earliest selectable date; earlier days are shown but disabled.maxmaxstring''Latest selectable date; later days are shown but disabled.default-valuedefaultValueCalendarValueundefinedThe initial selection of an uncontrolled calendar, restored on form reset. As an attribute,
several dates are separated by spaces and a range is written from/to.visible-monthsvisibleMonthsnumber1How many consecutive months are shown side by side.displayed-monthdisplayedMonthstring | undefinedundefinedThe first month shown, as any date in it. Setting it controls navigation: accept proposals in
onDisplayedMonthChange.default-displayed-monthdefaultDisplayedMonthstring''The first month shown initially, when it should not follow the selection or today.navigation-startnavigationStartstring''Earliest month navigation reaches, independent of the selectable dates.navigation-endnavigationEndstring''Latest month navigation reaches, independent of the selectable dates.minimum-selection-countminimumSelectionCountnumber0In multiple selection, the fewest dates the selection can shrink to.maximum-selection-countmaximumSelectionCountnumber—In multiple selection, the most dates that can be selected.minimum-nightsminimumNightsnumber0In range selection, the shortest range in nights.maximum-nightsmaximumNightsnumber—In range selection, the longest range in nights.range-exclusionrangeExclusion'reject' | 'restart''reject'What completing a range across an excluded date does: reject the click, or restart the range
there.paged-navigationpagedNavigationbooleanfalseMoves the previous and next controls by all visible months instead of one.disabled-navigationdisabledNavigation'disable' | 'hide''disable'Whether a previous or next control that cannot move is disabled or removed.show-outside-daysshowOutsideDaysbooleantrueShows the days of adjacent months that fill the first and last weeks.localelocalestring''Locale of captions, weekdays, day numbers and accessible dates, when it differs from the
page's.week-starts-onweekStartsOnnumber-1First day of the week, from 0 for Sunday to 6; negative follows the locale.todaytodaystring—The date marked as today.labellabelstring'Date'Accessible name of the calendar.caption-layoutcaptionLayoutCalendarCaptionLayout'label'The month caption as text, or as dropdowns for the month and year, the month only or the year
only.fixed-weeksfixedWeeksbooleanfalseAlways shows six weeks, so the height does not change between months.show-week-numbershowWeekNumberbooleanfalseAdds a column of week numbers, following the locale's week rules.button-variantbuttonVariantTpButton['variant']'ghost'Button variant of the previous and next month controls.
Properties
PropertyTypeDefaultDescriptionrenderDay((state: CalendarDayState) => unknown) | undefined—Renders passive content inside each day button from its state; return undefined for the plain
day number.formattersCalendarFormatters{}Display functions for the caption, dropdown entries, weekdays, day numbers and week numbers.messagesCalendarMessages{}Control names and validity messages, replacing the English texts.dayModifiersRecord<string, CalendarDateMatcher>{}Named sets or tests of dates, each marking its days with a data- attribute and a flag in the
day state.unavailableDatesCalendarDateMatcher—Dates that cannot be chosen because they are taken, such as booked nights; marked apart from
disabled dates.disabledDatesCalendarDateMatcher—Dates that are shown but cannot be focused or selected.hiddenDatesCalendarDateMatcher—Dates left as empty cells, keeping the grid's shape.calendarAdapterCalendarAdaptergregorianCalendarAdapterDate arithmetic and formatting, to show a calendar system other than the Gregorian one.onValueChange((event: TpValueChangeEvent<CalendarValue>) => void) | undefined—Called with each proposed selection before tp-value-change; a controlled owner accepts it by
setting value.onDisplayedMonthChange| ((event: CustomEvent<{ value: string; previousValue: string; sourceEvent: Event }>) => void)
| undefined—Called with each proposed month before tp-displayed-month-change; a controlled owner accepts
it by setting displayedMonth.selectionread-onlyCalendarValue—A copy of the committed selection.displayedMonthValueread-onlystring—The first month shown, as the ISO date of its first day.
Events
EventTypeDescriptiontp-value-changeTpValueChangeEvent<CalendarValue>Cancelable proposal of a new selection, before it commits; detail.value, detail.previousValue and detail.reason.tp-displayed-month-changeCustomEvent<{ value: string; previousValue: string; sourceEvent: Event; }>Cancelable proposal to show another month, from the navigation, the caption dropdowns or the keyboard; detail.value and detail.previousValue.
Methods
MethodReturnsDescriptionsetValue(value, sourceEvent?)booleanProposes a selection as if made on the grid, and returns whether it was committed.
Slots
SlotDescriptionfooterContent under the months, such as a summary of the selection; an empty footer takes no space.
CSS custom properties
PropertyDefaultDescription--tp-calendar-cell-size—Size of every day, weekday and navigation cell; raise it for richer day content.
Related
Components, widgets and patterns that use this one.