Calendar
Thymeleaf source
<sl:field th:field="*{dueDate}"> <sl:field-label>Due date</sl:field-label> <sl:calendar/></sl:field>Rendered HTML
<div class="field"> <label class="label field-label" for="dueDate">Due date</label> <div class="calendar" x-data="slCalendar" role="group" data-locale="en" data-week-starts-on="0" data-first-week-min-days="1"> <input type="date" class="input calendar-input" id="dueDate" name="dueDate" value="2026-10-23"> <template class="calendar-template"> <div class="calendar-nav"> <button type="button" class="calendar-nav-button calendar-previous" aria-label="Go to the previous month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m15 18-6-6 6-6"></path> </svg> </button> <button type="button" class="calendar-nav-button calendar-next" aria-label="Go to the next month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m9 18 6-6-6-6"></path> </svg> </button> </div> <label class="calendar-dropdown" data-unit="month"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Month"></select> </label> <label class="calendar-dropdown" data-unit="year"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Year"></select> </label> <span class="calendar-week-number-label">Week</span> </template> </div></div>Drawn over date inputs
Section titled “Drawn over date inputs”A calendar’s value lives in real <input type="date"> elements. The server renders them, and the slCalendar Alpine
component hides them and draws the month in their place. So th:field binds a calendar as it binds any input, and
the form submits ISO dates (2026-10-23). Without JavaScript, the date inputs show as they are and still work.
<form th:object="${task}" th:action="@{/tasks}" method="post"> <sl:field th:field="*{dueDate}"> <sl:field-label>Due date</sl:field-label> <sl:calendar/> <sl:field-error/> </sl:field></form>Without content, <sl:calendar> renders one date input itself. That input takes every attribute that is not a prop:
th:field, name, required, min, max, disabled, hx-*. class is the exception: it stays on the calendar.
LocalDate fields
Section titled “LocalDate fields”A date input only takes ISO dates, so the field has to be formatted as one. Do one of these:
- Annotate the field with
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE). - Set
spring.mvc.format.date=isofor the whole application.
Without either, Spring formats a LocalDate for the locale (10/23/26), which a date input shows as empty.
For a range, give the calendar two date inputs as content: the start, then the end. Each input binds its own field.
The calendar’s own attributes then stay on the calendar, so give it an aria-label that names the range, and give
each input a name of its own.
Thymeleaf source
<sl:calendar aria-label="Stay" number-of-months="2"> <sl:input type="date" th:field="*{checkIn}" aria-label="Check-in"/> <sl:input type="date" th:field="*{checkOut}" aria-label="Check-out"/></sl:calendar>Rendered HTML
<div class="calendar" x-data="slCalendar" role="group" data-number-of-months="2" data-locale="en" data-week-starts-on="0" data-first-week-min-days="1" aria-label="Stay"> <input class="input" type="date" aria-label="Check-in" id="checkIn" name="checkIn" value="2026-10-12"> <input class="input" type="date" aria-label="Check-out" id="checkOut" name="checkOut" value="2026-10-21"> <template class="calendar-template"> <div class="calendar-nav"> <button type="button" class="calendar-nav-button calendar-previous" aria-label="Go to the previous month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m15 18-6-6 6-6"></path> </svg> </button> <button type="button" class="calendar-nav-button calendar-next" aria-label="Go to the next month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m9 18 6-6-6-6"></path> </svg> </button> </div> <label class="calendar-dropdown" data-unit="month"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Month"></select> </label> <label class="calendar-dropdown" data-unit="year"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Year"></select> </label> <span class="calendar-week-number-label">Week</span> </template></div>The first pick sets the start. The second sets the end; if it comes before the start, the two swap. While only the start is picked, the band follows the pointer. A pick on a complete range starts a new one.
number-of-months shows several months side by side; on a narrow screen they stack.
Month and year selects
Section titled “Month and year selects”caption-layout="dropdown" replaces the month’s name with a select for the month and one for the year. This helps
for dates far from today, such as a date of birth. dropdown-months and dropdown-years show only one of the two.
The years run from start-month to end-month. By default those are the months of the inputs’ min and max, or
else 100 years back and 10 years ahead. Both also stop the previous and next buttons.
Thymeleaf source
<sl:field th:field="*{birthDate}"> <sl:field-label>Date of birth</sl:field-label> <sl:calendar caption-layout="dropdown" start-month="1926-01" end-month="2026-12"/></sl:field>Rendered HTML
<div class="field"> <label class="label field-label" for="birthDate">Date of birth</label> <div class="calendar" x-data="slCalendar" role="group" data-caption="dropdown" data-locale="en" data-week-starts-on="0" data-first-week-min-days="1" data-start-month="1926-01" data-end-month="2026-12"> <input type="date" class="input calendar-input" id="birthDate" name="birthDate" value="1990-06-15"> <template class="calendar-template"> <div class="calendar-nav"> <button type="button" class="calendar-nav-button calendar-previous" aria-label="Go to the previous month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m15 18-6-6 6-6"></path> </svg> </button> <button type="button" class="calendar-nav-button calendar-next" aria-label="Go to the next month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m9 18 6-6-6-6"></path> </svg> </button> </div> <label class="calendar-dropdown" data-unit="month"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Month"></select> </label> <label class="calendar-dropdown" data-unit="year"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Year"></select> </label> <span class="calendar-week-number-label">Week</span> </template> </div></div>Disabled dates
Section titled “Disabled dates”The inputs’ min and max disable the days outside them. disabled-dates disables more, comma-separated:
- a date:
2026-12-25; - a range:
2026-12-24..2026-12-31; - a day of the week:
sat,sun.
From a list in the model, join it: th:disabled-dates="${#strings.listJoin(booked, ',')}". A LocalDate prints as
an ISO date.
Thymeleaf source
<sl:field th:field="*{meetingDate}"> <sl:field-label>Meeting</sl:field-label> <sl:calendar min="2026-10-05" max="2026-11-27" disabled-dates="sat,sun,2026-10-14..2026-10-16,2026-10-28"/></sl:field>Rendered HTML
<div class="field"> <label class="label field-label" for="meetingDate">Meeting</label> <div class="calendar" x-data="slCalendar" role="group" data-locale="en" data-week-starts-on="0" data-first-week-min-days="1" data-disabled-dates="sat,sun,2026-10-14..2026-10-16,2026-10-28"> <input type="date" class="input calendar-input" min="2026-10-05" max="2026-11-27" id="meetingDate" name="meetingDate" value="2026-10-21"> <template class="calendar-template"> <div class="calendar-nav"> <button type="button" class="calendar-nav-button calendar-previous" aria-label="Go to the previous month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m15 18-6-6 6-6"></path> </svg> </button> <button type="button" class="calendar-nav-button calendar-next" aria-label="Go to the next month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m9 18 6-6-6-6"></path> </svg> </button> </div> <label class="calendar-dropdown" data-unit="month"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Month"></select> </label> <label class="calendar-dropdown" data-unit="year"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Year"></select> </label> <span class="calendar-week-number-label">Week</span> </template> </div></div>A disabled day still takes the focus, so the arrow keys move across it, but it cannot be picked. Only the calendar
knows about disabled-dates; a date input does not. So check the date on the server too, as you would any input.
Week numbers and the first day of the week
Section titled “Week numbers and the first day of the week”The page’s locale (Spring’s LocaleResolver) decides the first day of the week and how weeks are numbered. In most
of Europe weeks start on Monday and are ISO weeks; in the United States they start on Sunday, and the week of
1 January is week 1. week-starts-on overrides the first day: 0 is Sunday, 1 Monday, up to 6. show-week-number
adds a column with the week numbers.
The names of the months and days come from the browser (Intl), in the page’s language.
Thymeleaf source
<sl:field th:field="*{sprintStart}"> <sl:field-label>Sprint start</sl:field-label> <sl:calendar show-week-number week-starts-on="1"/></sl:field>Rendered HTML
<div class="field"> <label class="label field-label" for="sprintStart">Sprint start</label> <div class="calendar" x-data="slCalendar" role="group" data-show-week-number="true" data-locale="en" data-week-starts-on="1" data-first-week-min-days="1"> <input type="date" class="input calendar-input" id="sprintStart" name="sprintStart" value="2026-10-19"> <template class="calendar-template"> <div class="calendar-nav"> <button type="button" class="calendar-nav-button calendar-previous" aria-label="Go to the previous month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m15 18-6-6 6-6"></path> </svg> </button> <button type="button" class="calendar-nav-button calendar-next" aria-label="Go to the next month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m9 18 6-6-6-6"></path> </svg> </button> </div> <label class="calendar-dropdown" data-unit="month"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Month"></select> </label> <label class="calendar-dropdown" data-unit="year"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Year"></select> </label> <span class="calendar-week-number-label">Week</span> </template> </div></div>In a form
Section titled “In a form”th:field, on the calendar or on its field, binds the input. In a field, the input also takes the field’sid(the label points at it) andaria-describedby. The calendar takes the label of its input as its own name.- Errors. A field with errors marks the input
aria-invalid="true", and the field’s error describes the calendar. - Events. A pick fires
inputandchangeon the input, sohx-trigger="change"and your own listeners work as on a date input. The month and year selects fire nothing outside the calendar. - A value set by a script shows once the script fires
changeon the input, as it should for any input. - Form reset puts the inputs back to their rendered values, and the calendar shows them.
- A click on the label,
autofocusor the browser’s validation focus the input; the calendar passes the focus on to the day that takes it.
Thymeleaf source
<sl:field th:field="*{deliveryDate}"> <sl:field-label>Delivery</sl:field-label> <sl:calendar default-month="2026-10"/> <sl:field-error/></sl:field>Rendered HTML
<div class="field" data-invalid="true"> <label class="label field-label" for="deliveryDate">Delivery</label> <div class="calendar" x-data="slCalendar" role="group" data-locale="en" data-week-starts-on="0" data-first-week-min-days="1" data-default-month="2026-10"> <input type="date" class="input calendar-input" aria-invalid="true" id="deliveryDate" aria-describedby="deliveryDate-error" name="deliveryDate" value=""> <template class="calendar-template"> <div class="calendar-nav"> <button type="button" class="calendar-nav-button calendar-previous" aria-label="Go to the previous month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m15 18-6-6 6-6"></path> </svg> </button> <button type="button" class="calendar-nav-button calendar-next" aria-label="Go to the next month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m9 18 6-6-6-6"></path> </svg> </button> </div> <label class="calendar-dropdown" data-unit="month"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Month"></select> </label> <label class="calendar-dropdown" data-unit="year"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Year"></select> </label> <span class="calendar-week-number-label">Week</span> </template> </div> <div class="field-error" id="deliveryDate-error">must not be empty</div></div>Disabled and read-only
Section titled “Disabled and read-only”disabled on the input, or on a <sl:field-set> around it, disables the calendar: no day takes the focus.
readonly keeps the navigation but stops the picking.
Thymeleaf source
<sl:calendar name="closedDate" aria-label="Closed on" value="2026-10-23" disabled/>Rendered HTML
<div class="calendar" x-data="slCalendar" role="group" data-locale="en" data-week-starts-on="0" data-first-week-min-days="1"> <input type="date" class="input calendar-input" name="closedDate" aria-label="Closed on" value="2026-10-23" disabled> <template class="calendar-template"> <div class="calendar-nav"> <button type="button" class="calendar-nav-button calendar-previous" aria-label="Go to the previous month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m15 18-6-6 6-6"></path> </svg> </button> <button type="button" class="calendar-nav-button calendar-next" aria-label="Go to the next month"> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m9 18 6-6-6-6"></path> </svg> </button> </div> <label class="calendar-dropdown" data-unit="month"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Month"></select> </label> <label class="calendar-dropdown" data-unit="year"> <span class="calendar-dropdown-label"> <span></span> <svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"> <path d="m6 9 6 6 6-6"></path> </svg> </span> <select class="calendar-select" aria-label="Year"></select> </label> <span class="calendar-week-number-label">Week</span> </template></div>Keyboard
Section titled “Keyboard”The previous and next buttons are tab stops, and the month is one more. Within the month:
| Key | Moves the focus to |
|---|---|
| Arrow left, arrow right | The previous, next day (swapped on a right-to-left page) |
| Arrow up, arrow down | The same day a week earlier, later |
| Home, End | The first, last day of the week |
| Page Up, Page Down | The same day a month earlier, later (or the month’s last day when it is shorter) |
| Shift + Page Up, Shift + Page Down | The same day a year earlier, later |
| Enter, Space | Picks the day |
When the focus moves past the months that show, the calendar shows the next ones. A change of month is announced.
With htmx
Section titled “With htmx”- Back restores the calendar as htmx saved it, and
slCalendardraws it again. hx-trigger="change"on the calendar’s input (or on the form) sends the picked date.
Left out
Section titled “Left out”- Several separate dates. The inputs decide: one is a date, two are a range. Picking several dates is not here yet.
- A time zone. A date input holds a date without a time, so there is no time zone to get wrong.
- Custom drawing and formatting. The skins draw the calendar; the locale formats it. For content per day, use the month view.
For a button that opens the calendar in a popover, see the date picker.
Attributes
Section titled “Attributes”| Attribute | Values | Default | Description |
|---|---|---|---|
caption-layout | label dropdown dropdown-months dropdown-years | label | The month's caption: its name, or selects for the month and year, the month, or the year. |
number-of-months | number | 1 | How many months show side by side (stacked on a narrow screen). |
show-outside-days | boolean | true | Show the days of the previous and next month that fill the first and last week. |
show-week-number | boolean | — | Show the week numbers, counted as the page's locale counts them (ISO weeks in most of Europe). |
week-starts-on | number | — | The first day of the week: 0 is Sunday, 1 Monday, up to 6. By default the page's locale decides. |
default-month | string | — | The month shown first, as 2026-10. By default the month of the chosen date (or of the start of the range), or else the current month. |
start-month | string | — | The first month the calendar navigates to, as 2026-01; also the first year of the year select. By default the month of the inputs' min, or else 100 years ago. |
end-month | string | — | The last month the calendar navigates to, as 2026-12; also the last year of the year select. By default the month of the inputs' max, or else 10 years from now. |
disabled-dates | string | — | Days that cannot be picked, comma-separated: a date (2026-12-25), a range (2026-12-24..2026-12-31) or a day of the week (sat, sun). The inputs' min and max disable the days outside them too. Only the calendar knows: check the date on the server as well. |
Every prop can also be set from an expression: th:caption-layout="${...}". Any other attribute passes through to the rendered element, and class is appended.
JavaScript
Section titled “JavaScript”slCalendar sits on the calendar: <div class="calendar" x-data="slCalendar">.
- Without Alpine the date inputs show, and the form works with them.
- With it, the inputs stay in the form, hidden, and still submit, reset and validate.
slCalendaradds a.calendar-monthselement with the navigation and a<table role="grid">per month after them. - It needs no Alpine plugin. If you load Alpine yourself (
shadleaf.assets.alpine=external), plain Alpine is enough. - Keep your own
x-dataoff<sl:calendar>: an element has onex-data. Listen forchangeon the input instead.