Skip to the page
TACTILE UI

Kit part · Calendars & date pickers

Calendar

A month on a plate. The month and year sit on two drums behind a window and roll when you page; the chosen day is a latched cap, a range is a groove cut between two caps, and events are lamps named in the line below.

Element
<tui-calendar>
Needs
kit.css + kit.mjs
Rules
calendar.rules.md · 5
Finishes
Aluminium · Graphite · Porcelain · Black
Volumes
Tactile · Quiet
On this page
  1. Variants
  2. Every state, at once
  3. API
  4. Markup
  5. Accessibility
  6. How it’s built
  7. Rules
  8. Related parts
01

Variants

Range · a groove between two caps
Closed weekends, nothing before today
Locale · en-US, Sunday first
02

Every state, at once

Both finishes, side by side. Hover, pressed and focus are forced with .is-hover, .is-down and .is-focus; checked and disabled are real.

Aluminium

PartDayHoverFocusTodayEventChosenIn a rangeUnavailable
Day
12
12
12
12
12
12
12
12

Graphite

PartDayHoverFocusTodayEventChosenIn a rangeUnavailable
Day
12
12
12
12
12
12
12
12
03

API

NameKindValuesWhat it does
valueattribute"2026-09-28" · "start/end"The chosen day in ISO form; with mode="range", two dates split by a slash. A lone start while picking: "2026-09-10/".
modeattribute"single" · "range"One day, or a range picked with two presses.
monthattribute"2026-09"The month on the plate when there is no value. Also a property: set it to page from code.
min · maxattributeISO datesDays outside are struck through: focusable, never chosen. The page keys stop at them.
disabled-datesattributeISO dates, spacesSingle closed days.
disabled-weekdaysattribute"0 6"Closed weekdays, 0 Sunday to 6 Saturday.
localeattributeBCP 47 · langNames and the week’s first day, through Intl. Falls back to the nearest lang, then the browser.
first-dayattribute0–6Override the locale’s first day of the week.
todayattributeISO dateFix “today” (tests, screenshots). Otherwise the visitor’s own date.
empty-noteattribute"Nothing on"The words under the grid for a day with no events.
<data value data-lamp>child elementgreen · amber · redOne event: its day, its lamp, and its words as the text. Read when the calendar connects.
focusDate(iso)method—Page to that day’s month and move the tab stop there.
input · changeeventbubbleschange when a day, or a whole range, is chosen; input when a range’s start is.
--tui-calendar-cellCSS property40pxThe size of a day. 32px in narrow places; the calendar never grows wider than seven of them.
Arrowskeyboard±1 day · ±1 weekCrossing a month’s edge pages the plate.
Home · Endkeyboardweek start · endThe first and last day of the focused week.
PageUp · PageDownkeyboard±1 month · Shift ±1 yearThe same day, clamped to the month’s length.
Enter · Spacekeyboard—Choose the focused day.
04

Markup

<tui-calendar value="2026-09-28" locale="en-GB" min="2026-01-01">
  <data value="2026-09-03" data-lamp="green">Mix review · Ada</data>
  <data value="2026-09-09" data-lamp="amber">Hold · Studio B</data>
  <data value="2026-09-17" data-lamp="red">Clash: two bookings</data>
</tui-calendar>

<!-- A range: two caps and the groove between. -->
<tui-calendar mode="range" value="2026-09-10/2026-09-14"></tui-calendar>
import '/kit/kit.mjs';   // defines <tui-calendar>

const cal = document.querySelector('tui-calendar');
cal.addEventListener('change', () => load(cal.value));   // "2026-09-17"
cal.month = '2026-10';                                    // page it from code
cal.focusDate('2026-10-02');                              // and move the tab stop

Needs kit.css and kit.mjs. How to install the kit.

05

Accessibility

  • The grid is a role="grid" table labelled by the drum window, which reads the month and year and is a polite live region: paging says “October 2026”.
  • One day holds the tab stop; arrows move it. Each day is named in full (“Thursday 17 September 2026, Tracking · Room 2, Clash: two bookings”), with “today” and “unavailable” where true.
  • Chosen days carry aria-selected="true", today aria-current="date". Chosen is also the contrast cap and a heavier figure, not only a colour.
  • Every lamp has its words: in the day’s name, and printed in the line under the grid for the day in focus.
  • The page keys name where they go (“Next month, October 2026”) and are disabled past min and max.
06

How it’s built

A custom element that builds its own light DOM: a head with two small keys and the drum window, a role="grid" table of six weeks (blank cells before the 1st and after the last, so the plate never changes height), and the note line. Dates are whole days since 1970 in UTC, so no time zone can move one. Each drum is a strip of three faces (last, this and next) in a recess, shaded like a cylinder; paging re-prints the strip one face off and animates it back with the Web Animations API over 220ms (zero under reduced motion), so the old month rolls away and the new one comes round. The chosen day takes the contrast cap and the pressed shadow; a range’s groove is a ::before strip of the well under the days, half-width under its two caps.

07

Rules

calendar.rules.mdIn the brain

Lamps mark the days; the words live in the markup and in the line under the grid.

5 rules in this file, with the foundations they rest on and the anti-patterns that break them. Full rules ship in the brain with the Pass.

08
28 Sept 2026
24252627282930

Date picker

A date field with its calendar card in a slot underneath: type a date in any common form for your locale, or press the key and the card slides out. Single dates and ranges; shown the Intl way, posted as ISO.

Reel 342:10
Mixed0:30

Table

Rows of data with printed column labels, figures right-aligned in columns, and status in lamps with words.

Lamp

The only colour in the kit: a lamp that is on. Green is on, amber is waiting, red is recording or failing, blue is linked, white is standing by. Always next to words.

Docs
Finish
Volume