Calendar

There are two calendars, drawn from the same days and keys.

  • ds-calendar draws a month the way a list draws its items: seven days across, a week to a row, each day a hairline box four squares wide and four high. Its date stands on the first square, two lines of events below it and one square to spare. With compact, a day is one square and the month becomes the small one, one of twelve in a year. With picker, it is a field that opens that small month to choose a date: see the picker.
  • ds-calendar-large draws months side by side as squares: a week to a column and the seven days down it, one and a half times the grid, in a rounded frame that scrolls sideways. A day on which something happens is filled with its colour, so a long piece of work reads as a band across the weeks.
A quiet field journal

A compact fixed calendar puts day numbers on the small square grid.

A writing streak

Two months lit by how much was written each day, as a contribution graph.

Arrival and departure

One picker chooses the stay: arrival and departure in the same month, written as one value.

A month, four squares a day

Press a day to select it, or move with the arrow keys and Page Up and Page Down. The third event on a day folds into +2; press it to unfold them.

A small month, one square a day

compact draws a month one square a day, as a year of months is drawn. With events, the days that have one keep the ink.

Heat, as a contribution graph

heat lights each day by how much happens on it: the events’ counts added up, in four steps of the busiest day. Quiet days stay plain.

One day or a range, two squares a day

Press a day to choose it; press another to end a range from it, and the pointer previews the end. The press after starts again. Each press fires change.

Pick a day or a range

Open it with a click, Enter or the down arrow. Press a first day and a last; press one day twice, or leave, for a single day. Escape closes it.

Months in squares

Weeks run sideways and the days down them. Scroll, or use the arrows and Today; a day several things share is split into bands.

<ds-calendar month="2026-10" selectable week-numbers></ds-calendar>
const calendar = document.querySelector("ds-calendar");
calendar.events = [
{ title: "Design review", start: "2026-10-03", time: "10:00", color: "var(--ds-zenith-blue)" },
{ title: "Launch", start: "2026-10-12", end: "2026-10-14", color: "var(--ds-apple-green)" },
];
calendar.addEventListener("change", event => console.log(event.detail.value));
MeasureSize
A dayFour squares wide (day-width), its date’s square, lines squares of events (two), and one square to spare.
The title and arrowsOne square, one square above the days. The title is a plain button, its words alone with no chevron, one control high, filled by the pointer 4px past its words at each side (see months and years). Today and the arrows stand at the end: each arrow a chevron on a square of its own, one control high, in the title grey until the pointer fills it.
The weekday namesOne square, in the title grey, at the start of each day.
A dateAt the end of its first square, in the title grey, tabular figures.
TodayA disc nine tenths of a square across, in red for blood, the date in white.
The selected dayA square of the same size with the system’s 4px corners, in Sharp blue, the date in white.
A rangeEach end drawn as the selected day, the days between on a band of the blue at 16%. In a compact month the band is the marks’ height on their line, from the middle of the first day to the middle of the last, so the ends and the band read as one bar.
An eventOne square, drawn one pixel short, its words 4px from its edges; the time first, fainter. Filled with its color or the fill.
More events than linesThe last line says +2; pressing it lays the day’s events over the days below.
A week numberW42 in the compressed face, at the start of the week’s first day, with week-numbers.
Lines1px in the line colour, centred on the grid lines.

A day outside the month is an empty box. fixed always draws six weeks, so moving between months never changes the calendar’s height.

compact makes every day a square, one square across by default: the date centred in it, the weekday names a letter each, the month’s name shortened, and no events written out. With day-width="2" each day is two squares each way, the weekday names take two letters, Today stands beside the arrows and the marks grow to a square and two thirds, for a month with more room. outside-days writes the days of the months before and after in the title grey; pressing one goes there. When the calendar has events, a day with one keeps the ink and a day without falls back to the title grey, so a year of small months shows where things happened.

<ds-calendar compact heat fixed></ds-calendar>
calendar.events = [
{ title: "3 changes", start: "2026-07-08", count: 3 },
{ title: "12 changes", start: "2026-07-09", count: 12 },
];

heat lights each day by how much happens on it, as a contribution graph does. A day’s amount is its events’ counts added up (an event without one counts once, an event over several days counts on each). The busiest day of the month shown sets the scale, and every other day takes one of four steps of it: up to a quarter, a half, three quarters, or more. heat-max fixes the amount that lights a day fully, so months stay comparable. A day with nothing stays plain, and when the month has events at all its date falls back to the title grey; the neighbouring months’ days are never lit.

StepLight paperDark paperThe date
1The accent’s pastelIts dimThe ink; white on dark paper
2Halfway to the accentHalfway to the accent’s dark versionThe ink; white on dark paper
3The accent (Apple green)The accent’s dark versionThe ink
4The accent deepened with 40% of the inkThe full accentThe ink

The fill is the date’s mark, the same size as today’s disc and the selected square, with a picture’s corners, so it stays inside the day’s square and the grid; in a compact month the marks stand a pixel apart, as a contribution graph’s squares do. Every step keeps its date at 4.5:1 or more. Today and the selection draw over the heat. The fills are the palette’s tokens (--one-apple-green-pastel, --one-apple-green), each light-dark(); a page lights the month in another colour by setting --ds-calendar-heat-1 … -4 and their --ds-calendar-heat-ink-1 … -4 from the palette.

<ds-calendar-large start="2026-09" months="6"></ds-calendar-large>
MeasureSize
A dayOne square times scale (one and a half: 30px).
Lines1px times the scale, inside a frame whose corners are four tenths of a day.
WordsThe heading size times the scale (19.5px).
The weekday namesDown the start, outside the scroll, every other one named; a letter each on a narrow screen.
The monthsEach name over the week holding its first day; January and the first month carry the year.
A dateWritten only on today, the selected day, and where an event starts or ends.
TodayA disc nine tenths of a day across in red for blood; the selected day a square of the ink.
A filled dayThe event’s color, Apple green by default; days two or more events share are split into equal bands.

The title names the month at the start of the view as it scrolls. The arrows move a month at a time and Today scrolls to today’s month; past the months drawn, start moves with them and navigate says so.

<ds-calendar compact selectable day-width="2" value="2026-10-14/2026-10-20"></ds-calendar>

There is one way to choose, in a month and in the picker: a person picks one day or a range in the same calendar, with nothing to switch. The first press chooses a day, and value is that day. The next press ends a range from it, before or after, and value becomes start/end (ISO 8601’s interval); pressing the same day again keeps the one day. The press after that starts again. While the end is still to come, the day under the pointer previews it.

change fires with each press that chooses: { value, previous, start, end }, end empty while it is still to come and the last day once it is chosen. A value set by the page is whole, so the next press starts again.

A selectable calendar shows Clear while something is chosen: a secondary button one control high, in the shared word, centred on the square right under the dates. With nothing chosen it is not there. It empties the value and fires change with value empty (start and end too). clear() does the same from code, in a month and in a picker (where it also closes the panel). required leaves Clear out; the picker’s month uses the picker’s own Clear.

Pressing the title travels by months and years, as a date picker’s title does. The days go up to the twelve months of their year (the title reads the year and the arrows step a year), the year goes up to twelve years (the title reads them, 2016 – 2027, and the arrows step twelve). Choosing a year shows its months; choosing a month shows its days and fires navigate. Nothing is chosen on the way.

MeasureSize
A month or a yearOne square high and whole squares wide, three across a narrow month (two squares each) and four across a wider one, rows one square apart.
Its markNine tenths of a square high with a picture’s corners: the chosen month or year in Sharp blue, today’s in the today red, the pointer’s in the fill.
The areaThe days’ height, so nothing below moves between levels; the level a person moves to fades in (opacity and scale, none with reduced motion).

The months and years are a grid with one Tab stop: the arrow keys move (past December is the next year’s January), Enter or Space chooses, and Escape goes back down a level without closing a picker. Month names and years come from the current language through format.ts. no-header leaves the travel out with the title.

<form>
<ds-calendar picker name="when" label="When" literal min="2026-10-01"></ds-calendar>
</form>
picker.addEventListener("change", event => {
console.log(event.detail.value); // "2026-10-21", "2026-10-21/2026-10-24", "2026-10-21T09:00/2026-10-21T10:00", or "" when cleared
});

With picker, the calendar is a field one control high, its value written in it and a chevron (chevron-down) at its end. A range is one picker and one value (Oct 21 – 24, 2026), never two pickers side by side for its ends. Pressing it opens a panel half a square below, on the menu’s frame: the first and last day written out where they can be typed, the compact month one square a day, an Include time switch and Clear.

It chooses as a month does. Each opening starts a new choice: the first day pressed is the value; the second, before or after, ends a range and closes the panel. Pressing the same day twice, or leaving after the first press, keeps one day.

The value is ISO 8601: a day, a day with its time, or two of them joined by a slash, which is also what a form sends. A value with a time turns Include time on; time sets it from the page.

ValueFace
2026-10-21Oct 21, 2026
2026-10-21/2026-10-24Oct 21 – 24, 2026
2026-10-21T09:00/2026-10-21T10:00Oct 21, 2026, 9:00 – 10:00 AM

The face writes the value in the current language, a range as the language writes one. length writes a day short, medium (the default) or long; before a day is chosen the face reads placeholder, the shared words “Choose a date” by default, in the title grey.

MeasureSize
The fieldA field’s face: one control high at the foot of its square, as wide as its words (it hugs them, as a button does; --ds-calendar-picker-width sets a width), the words 4px in, the chevron-down icon (16px) 2px from the end.
The days to typeA field one control high on its own square for the first day and one for the last; with times on, each day’s time on the square below it. One square spare after them.
The monthOne square a day, seven squares wide: the month with its chevrons, the weekdays in a letter, six weeks with the neighbouring months’ days in the title grey.
The marksThe chosen days are squares with 4px corners in Sharp blue, the days between on a band of it, today a red disc.
Include time, ClearOne square each, the switch at the end, set off by one-square separators with a line through them.
The frameA 1px line with half a square inside it, no shadow: eight squares wide.
Its placeHalf a square below the field, or above it when there is more room there, kept on screen.

Turning times on gives the first day 9:00 and the last 10:00, which the time fields change; with times on, the panel stays open after the second press so they can be set.

A typed day is read when the person presses Enter or leaves the field: 2026-10-21, or a date the browser can read such as “Oct 21, 2026”. Anything else puts the field back as it was.

required leaves Clear out and tells the form a value is missing. min and max limit the days. A form’s reset puts back the value the field started with. readDateValue, writeDateValue and parseTypedDay are exported for a page that reads or writes the value itself.

Both calendars take the same events, as a property:

FieldWhat it is
titleWhat happens.
startThe first day, YYYY-MM-DD.
endThe last day, for something over several days.
timeWhen it starts, written before the title in a month.
colorA token or colour for its fill. Take it from the palette.
countHow much it counts for in heat; one when left out.
idAnything the page needs back; the calendar leaves it alone.

A day lists its events longest first, then by time.

A day is written YYYY-MM-DD and stays on its date in every time zone. The week starts where the language starts it (Monday in most of Europe, Sunday in the United States and Japan); week-start sets it, 0–6 from Sunday or a day’s English name. Month and weekday names come from the current language.

min and max limit the days a person may choose: the days outside them are drawn faint and cannot be selected. today draws another day as today, for a preview or a test.

The day arithmetic the calendars use is DS1/0-face/calendar.ts, plain TypeScript without Lit: monthWeeks, addDays, addMonths, firstWeekday, weekdayNames, isoWeek, eventsByDay and the rest.

  • Each calendar is a grid named by its month (or label), with one Tab stop on a day: the one moved to, the selected day, today, or the first. A selectable calendar is aria-multiselectable, and every day of the range is aria-selected.
  • The arrow keys move a day, or a week across the weeks; Home and End go to the week’s first and last day; Page Up and Page Down move a month, with Shift a year. A month moves on when the focus leaves it, and right to left the arrows turn round.
  • Enter or Space presses the day. Each day is labelled with its whole date and what happens on it, and today is aria-current="date"; a selectable calendar marks the selected day with aria-selected.
  • The arrows are named “Previous month” and “Next month” (“Previous year”, “Next year”, “Earlier years” and “Later years” among the months and years), and the month’s name is announced when it changes. The title button is named by the title and “Choose a month and year”. All the words are the shared words, in every language.
  • A picker’s field is a button with aria-haspopup="dialog" and aria-expanded, named by label and the value. Enter, Space or the down arrow opens the panel with the focus on the first chosen day (or today); a click opens it and leaves the focus on the field. The typed fields and times are named by the label and which end they are; the switch is role="switch". Escape, a press outside, or the focus leaving closes the panel, and the focus goes back to the field.
AttributeTypeDefaultWhat it does
monthstringThe value’s month, or today’sThe month shown, YYYY-MM.
valuestring""The selected day or range; in a picker, with or without times.
selectablebooleanfalsePressing a day selects it, or ends a range.
pickerbooleanfalseA field that opens the small month.
compactbooleanfalseOne square per day.
day-widthnumber4 (1 compact)Squares per day.
linesnumber2Lines of events under a date.
week-numbersbooleanfalseThe week’s number on its first day.
fixedbooleanfalseAlways six weeks.
outside-daysbooleanfalseThe days of the months before and after, in the title grey.
heatbooleanfalseLight each day by how much happens on it.
heat-maxnumberThe busiest day shownThe amount that lights a day fully.
no-headerbooleanfalseLeave out the title and arrows.
no-todaybooleanfalseLeave out the Today button.
min, maxstring—The days a person may choose.
week-startstringThe language’sThe week’s first day.
todaystringThe reader’s todayThe day drawn as today.
labelstringThe month (a picker: “Date”)The grid’s or the picker’s name; literal shows it as written.
namestring—The form field’s name.
placeholderstring“Choose a date”A picker’s words before a day is chosen.
lengthstringmediumHow a picker writes a day.
timebooleanfalseA picker chooses a time with each day (Include time).
requiredbooleanfalseA form needs a value; Clear is left out.
disabledbooleanfalseA picker cannot be opened.
openbooleanfalseA picker’s panel is open.
AttributeTypeDefaultWhat it does
startstringThe month before today’sThe first month drawn, YYYY-MM.
monthsnumber6How many months are drawn.
scalenumber1.5How many times the grid it is drawn.
value, selectable, min, max, week-start, today, label, no-headerAs in a month.
EventDetailWhat it means
day-press{ day, events }A day was pressed, or chosen with Enter or Space. Cancel it to keep a selectable calendar’s value.
change{ value, previous, start, end }A person selected a day, or the end of a range.
input{ value, previous, start, end }A picker’s value changed, the same moment as change.
ds-calendar-toggle{ open }A picker’s panel opened or closed.
event-press{ event, day }An event was pressed (in a month).
navigate{ month }A person moved to another month.
MethodWhat it does
previousMonth(), nextMonth()Moves a month, as the arrows do.
showToday()Brings today’s month into view.
showMonth(month)Shows a month (ds-calendar).
clear()Empties the value, as Clear does, and fires change.
show(), hide()Opens or closes a picker’s panel.
PartWhat it is
header, title, previous, next, todayThe title row and its buttons.
picks, pick, foot, clearThe months or years, one of them (with selected and today), and Clear’s row and button.
grid, weekdaysThe grid and its weekday names.
day, week, event, moreA month’s days, weeks, events and the +2 line; a day also carries today, selected and heat.
cell, frame, scrollerThe large calendar’s days, frame and scroller.
trigger, popover, field, options, clearA picker’s field, its panel, a row of a day or a time to type, and Include time and Clear.
Custom propertyDefaultWhat it sets
--ds-calendar-todayvar(--ds-red-for-blood)Today’s disc.
--ds-calendar-selectedvar(--ds-sharp-blue)The selected day and a range’s ends; the band between is it at 16%.
--ds-calendar-event-fillThe fill (Apple green in the large calendar)An event without a colour.
--ds-calendar-line1px solid var(--ds-border-color)A month’s lines.
--ds-calendar-inkvar(--ds-text-color)The large calendar’s lines and selected day.
--ds-calendar-picker-widthIts words’ widthA picker’s field.
--ds-calendar-heat-1 … -4The accent’s four stepsHeat’s fills.
--ds-calendar-heat-ink-1 … -4The ink, or white on the dark stepsThe date on each step.

Use activity on ds-calendar-large for a compact contribution-style grid. It defaults to one grid square per day, separates the squares, and fills dates with their event colours. Dates and event descriptions remain available to assistive technology and in a tooltip. Use no-header for a fixed month.

<ds-calendar-large activity no-header start="2026-10" months="1"></ds-calendar-large>

Set events as a property, as with the full calendar. Days without events stay quiet; repeated events on one day do not increase the colour intensity.