Skip to main content
xriptxr

Calendar

Info:Form
87

A month grid that selects a day, a set of days, or a range — locale-aware, keyboard-complete, and popup-free.

date month grid datepicker day picker range schedule agenda availability

Live demo

live · @xtyle/astro

Calendar

Three modes

single replaces the pick, multiple toggles a set, and range opens on the first day and closes on the second — sweeping the pointer (or the arrow keys) across the grid previews the pending half before you commit it.

single

July 2026
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311

value: 2026-07-14

multiple

July 2026
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311

value: 2026-07-07,2026-07-09,2026-07-21

range

July 2026
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311
2345678

value: 2026-07-13,2026-07-19

Days you can mark

decorations hangs event dots and a busy bar on any day — real elements in the fragment, not a stylesheet's pseudo-element — and each mark's label rides into that day's accessible name, so the dot is announced and not merely seen. Bounds, a disabled list, and an isDateDisabled predicate (here: no Mondays) compose into which days are refused.

July 2026
WkSunMonTueWedThuFriSat
262829301234
27567891011
2812131415161718
2919202122232425
302627282930311
312345678

Dots are events; the bar is a fully-booked day.

Days before the 5th and after the 27th are out of bounds, every Monday is refused by the predicate, and the 22nd is refused by name.

Picked: nothing yet

The locale decides

Month names, weekday names, the numbering system, and where the week starts all come from Intl — no locale table ships with the component, and no date library. Pass firstDayOfWeek to override the locale outright.

en-US

July 2026
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311

week opens Sunday

en-GB

July 2026
MonTueWedThuFriSatSun
293012345
6789101112
13141516171819
20212223242526
272829303112

week opens Monday

fr-FR

juillet 2026
lun.mar.mer.jeu.ven.sam.dim.
293012345
6789101112
13141516171819
20212223242526
272829303112

French month names

ja-JP

2026年7月
2829301234
567891011
12131415161718
19202122232425
2627282930311

Japanese numerals and names

The keyboard grid

The grid is one tab stop; inside it the cursor roves. Tab into the calendar above and drive it — every key of the WAI-ARIA date-grid pattern is here, and the arrows walk straight out of one month and into the next.

← →

a day at a time, straight across week and month boundaries

↑ ↓

a week at a time

Home / End

the first and last day of the week (wherever the locale starts it)

PageUp / PageDown

the previous and next month

Shift + PageUp / PageDown

the previous and next year

Enter / Space

select the focused day

Escape

cancel a half-drawn range

Tones and sizes

The selected day and the range band speak the full tone vocabulary — 21 in all, every one AA-cleared — and three sizes scale the cells against the type ramp.

tone="accent"
July 2026
SunMonTueWedThuFriSat
1234
567891011
12131415161718
19202122232425
262728293031
tone="accent-2"
July 2026
SunMonTueWedThuFriSat
1234
567891011
12131415161718
19202122232425
262728293031
tone="accent-3"
July 2026
SunMonTueWedThuFriSat
1234
567891011
12131415161718
19202122232425
262728293031
tone="accent-4"
July 2026
SunMonTueWedThuFriSat
1234
567891011
12131415161718
19202122232425
262728293031
tone="success"
July 2026
SunMonTueWedThuFriSat
1234
567891011
12131415161718
19202122232425
262728293031
tone="warn"
July 2026
SunMonTueWedThuFriSat
1234
567891011
12131415161718
19202122232425
262728293031
tone="danger"
July 2026
SunMonTueWedThuFriSat
1234
567891011
12131415161718
19202122232425
262728293031
tone="purple"
July 2026
SunMonTueWedThuFriSat
1234
567891011
12131415161718
19202122232425
262728293031
Small
July 2026
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311
Medium (default)
July 2026
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311
Large
July 2026
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311

Read-only and disabled

A readonly calendar still navigates and announces — a published schedule you can walk but not edit. A disabled one is wholly inert: no tab stop, no steps, no picks.

readonly
July 2026
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311
disabled
July 2026
SunMonTueWedThuFriSat
2829301234
567891011
12131415161718
19202122232425
2627282930311

Calendar is the month grid itself: a controlled role="grid" of day cells with no opinion about being inside a popup (that is DatePicker's job, and it consumes this). Three modes: single picks one day, multiple toggles a set, and range opens on the first pick and closes on the second, previewing the pending half as the pointer — or the keyboard cursor — sweeps across the grid.

Every date is a wall-clock date, an ISO YYYY-MM-DD civil day rather than an instant, so nothing here can shift a selection across a DST boundary; today is read in the host's timezone, or whichever IANA zone timezone names. The locale drives the month and weekday names, the numbering system, and the first day of the week, all through Intl (Intl.Locale's weekInfo where the engine has it, ISO-8601 otherwise) — no bundled locale table and no date library — and firstDayOfWeek overrides it outright. min / max, a disabledDates list, and an isDateDisabled predicate compose into which days are refused, and decorations hangs event dots, a busy bar, and extra announced text on any day. The keyboard is the full WAI-ARIA date grid: arrows walk days and weeks straight across month boundaries, Home / End snap to the week's edges, PageUp / PageDown step months (Shift for years), and Escape cancels a half-drawn range.

When to use

How this component composes with the rest of the set.

Drop it inside a Popover (or reach for DatePicker, which already does) — the calendar has no opinion about being in one.
Feed decorations from your event store to mark busy days; give every mark a label so the dot is announced, not just seen.
Pair mode="range" with fixedWeeks so the grid does not resize under the pointer mid-sweep.
Use readonly for a published schedule: the grid still navigates and announces, but nothing can be picked.

Props

22 props, straight from the manifest.

PropTypeDefaultBindingsDescription
mode "single" | "multiple" | "range"
single multiple range
single
html svelte astro
What a click does: replace the selection, toggle a day in a set, or draw a range.
value string
html svelte astro
The selection as a comma-separated ISO list. `single` holds one date, `multiple` holds N, `range` holds `start` while pending and `start,end` once closed.
month string
html svelte astro
The displayed month, `YYYY-MM`. Uncontrolled by default (seeded from the selection, else today) and reflected as it steps; set it to drive the month from outside.
min string
html svelte astro
The earliest selectable day, ISO. Earlier days are disabled and the previous-month step goes inert.
max string
html svelte astro
The latest selectable day, ISO.
locale string
html svelte astro
A BCP-47 tag driving the month and weekday names, the numbering system, and the first day of the week. Defaults to the host's.
timezone string
html svelte astro
An IANA zone that pins what "today" means (a venue's zone rather than the visitor's). It shifts nothing else: dates are wall-clock, not instants.
firstDayOfWeek number
html svelte astro
Override the week's first day, 0 = Sunday … 6 = Saturday. Defaults to the locale's, via `Intl`.
weekdayFormat "short" | "narrow"
short narrow
short
html svelte astro
How the weekday headers are abbreviated. The full name always rides in `abbr` for assistive tech.
weekNumbers boolean false
html svelte astro
Add the week-of-year row header.
fixedWeeks boolean false
html svelte astro
Always draw six rows, so the grid's height never jumps between months.
hideOutsideDays boolean false
html svelte astro
Blank the adjacent-month days instead of rendering them (they are selectable when shown).
hideNav boolean false
html svelte astro
Drop the previous / next steps, leaving the month heading — for a picker that drives the month itself.
disabledDates string[]
html svelte astro
Days to refuse, as ISO dates. Composes with `min` / `max` and `isDateDisabled`. On the HTML element the attribute takes a comma-separated list.
isDateDisabled (iso: string) => boolean
html svelte
A predicate, set as a property, run once per rendered day. The seam for a rule a list can't express (no Mondays, no past weekends, no blackout window).
decorations Record<string, { dots?: string[]; busy?: boolean; label?: string }>
html svelte astro
Per-day marks, keyed by ISO date: `dots` (one per event, each a tone name), `busy` (a bar), and `label` (appended to the day's announcement, so a mark is never visual-only). A property, or a JSON string on the attribute.
tone FullTone
accent neutral danger success warn info accent-2 accent-3 accent-4 red orange yellow green blue purple brown pink cyan gray white black
accent
html svelte astro
Color of the selected day and the range band, any of the 21 tones.
size Size
sm md lg
md
html svelte astro
Type scale and cell size.
readonly boolean false
html svelte astro
The grid navigates but never changes: a published schedule you can read and walk, not edit.
disabled boolean false
html svelte astro
Inert: every day refused, both month steps disabled, and no tab stop in the grid.
label string
html svelte astro
The accessible name of the calendar as a whole ("Departure date"). The grid itself is named by the month.
labelledby string
html svelte astro
The id of an existing element naming the calendar, instead of `label`.

Appearance

Variants

single

.xtyle-calendar--single

One day at a time; a second pick replaces the first.

multiple

.xtyle-calendar--multiple

A set of days, each click toggling one in or out.

range

.xtyle-calendar--range

Two ends and the band between them, with the pending half previewed under the pointer.

Sizes

sm

.xtyle-calendar--sm

Compact — for a dense sidebar or a popup.

md

default
.xtyle-calendar

Default.

lg

.xtyle-calendar--lg

Large — a page-level scheduler.

States

selected

.xtyle-calendar__cell[data-filled]

A picked day: a filled pill in the tone. The whole of a range is aria-selected="true" — it is all picked — but only its two ends fill; the days between carry the band, so the selection reads as one span rather than a row of identical pills.

in-range

.xtyle-calendar__cell[data-in-range]

A day inside a closed range: the soft band, edge-to-edge across the row.

preview

.xtyle-calendar__cell[data-preview]

The pending half of a range, following the pointer (or the keyboard cursor, so the preview is not mouse-only).

today

.xtyle-calendar__cell[data-today]

Today, ringed rather than filled, and marked aria-current="date".

outside

.xtyle-calendar__cell[data-outside]

An adjacent-month day: dimmed, still selectable, and picking one steps the grid onto its month.

disabled

.xtyle-calendar__cell[aria-disabled="true"]

A refused day — outside min/max, listed, or caught by the predicate. Still focusable, never selectable.

focus-visible

.xtyle-calendar__cell:focus-visible

The keyboard cursor: a token-colored ring, plus a transparent outline promoted to a real one in forced-colors mode.

Anatomy

The named parts that make up the component, with their selectors.

calendar

.xtyle-calendar

The root; carries the tone, the size, and the mode. Becomes a labelled group when given a label.

--font-sans --text-sm --leading-normal --fg-1

header

.xtyle-calendar__header

The month bar: the previous / next steps around the month heading.

--space-1

title

.xtyle-calendar__title

The month and year, localized. An aria-live="polite" region, so stepping the month announces the new one.

--fg-0 --text-body --weight-medium

nav

.xtyle-calendar__nav

A month step. Marked aria-disabled and made inert when the next month is entirely past min / max.

--fg-1 --fg-disabled --state-hover --radius-sm --ring

weekday

.xtyle-calendar__weekday

A column header, rotated to the locale's first day. Carries the full weekday name in abbr.

--fg-3 --text-xs --weight-medium

weeknum

.xtyle-calendar__weeknum

The optional week-of-year row header, generalized from ISO-8601 over the locale's week rules.

--fg-3 --text-xs

cell

.xtyle-calendar__cell

A day: the gridcell that carries the roving tab stop, the selection state, and the range band.

--radius-sm

day

.xtyle-calendar__day

The pill inside a cell — the box that fills when the day is selected and rings when it is today.

--radius-sm --border-thin --state-hover --duration-fast --ease-standard

dot

.xtyle-calendar__dot

A decoration dot under the day number, one per event, colored by tone. A real element, so a mod can reshape it.

--radius-full

busy

.xtyle-calendar__busy

The bar marking a fully-booked day.

--fg-2 --border-thick --radius-full

cue

.xtyle-calendar__cue

The non-color selection cue. Drawn only when the theme's --selection-cue resolves to marker (a high-contrast or redundant-cues algorithm), so a selected day never rests on color alone.

--selection-cue --border-thick --radius-full

Tokens & coverage

What the component consumes, checked live against what the algorithm produces.

Success:fully covered 87/87 consumed tokens produced default register: 305 tokens

Live coverage check against the xtyle-default register (derive(xtyleDefault, { anchors })coverComponent(manifest, register)). Every token this component consumes must be a key the algorithm produces.

--accent --accent-2 --accent-2-bg --accent-2-fg --accent-3 --accent-3-bg --accent-3-fg --accent-4 --accent-4-bg --accent-4-fg --accent-bg --accent-fg --black --black-bg --black-fg --blue --blue-bg --blue-fg --border-normal --border-thick --border-thin --brown --brown-bg --brown-fg --cyan --cyan-bg --cyan-fg --danger --danger-bg --danger-fg --duration-fast --ease-standard --fg-0 --fg-1 --fg-2 --fg-3 --fg-disabled --font-sans --gray --gray-bg --gray-fg --green --green-bg --green-fg --info --info-bg --info-fg --leading-normal --neutral --neutral-bg --neutral-fg --orange --orange-bg --orange-fg --pink --pink-bg --pink-fg --purple --purple-bg --purple-fg --radius-full --radius-sm --red --red-bg --red-fg --ring --selection-cue --space-0 --space-1 --state-hover --success --success-bg --success-fg --text-body --text-sm --text-xs --warn --warn-bg --warn-fg --weight-medium --weight-semibold --white --white-bg --white-fg --yellow --yellow-bg --yellow-fg

Accessibility

The month is a role="grid" named by its heading, with each day a gridcell carrying its full localized date as the accessible name — so a screen reader announces "Wednesday, July 1, 2026", never a bare "1".
The full WAI-ARIA date-grid keyboard: arrows move by day and by week (straight across week, month, and year boundaries), Home / End snap to the week's edges, PageUp / PageDown step months and Shift + either steps years, Enter / Space select, and Escape cancels a half-drawn range.
One roving tab stop: the grid is a single stop in the tab order, and the cursor is re-seated onto the equivalent cell after every repaint, so a month step never drops focus to the body.
The month heading is an aria-live="polite" region, so stepping the month announces it even though focus stays on the step button.
Selection state rides aria-selected, today rides aria-current="date", and a refused day rides aria-disabled — still focusable (so the keyboard can pass over it) but never selectable.
Under a theme whose --selection-cue is marker, the selected day gains a non-color cue, so selection never rests on color alone.
A decoration's label is appended to its day's accessible name, so an event dot or a busy bar is announced rather than being a visual-only mark.

Code

Modes, limits, and day decorations

A single-day grid and a bounded range with week numbers, a disabled-day predicate, and dots and busy bars hung on days.

<!-- a single day, with the locale deciding where the week starts -->
<xtyle-calendar id="cal" mode="single" value="2026-07-14" locale="en-GB"></xtyle-calendar>

<!-- a range, six fixed rows so the grid never jumps, weekends and events marked -->
<xtyle-calendar id="trip" mode="range" fixed-weeks week-numbers min="2026-07-01" max="2026-08-31"></xtyle-calendar>

<script>
	const trip = document.getElementById("trip");

	// a predicate says which days are refused — here, every Monday
	trip.isDateDisabled = (iso) => new Date(iso + "T00:00:00Z").getUTCDay() === 1;

	// decorations mark days: dots for events, a bar for a full day, text for the announcement
	trip.decorations = {
		"2026-07-16": { dots: ["accent", "success"], label: "2 events" },
		"2026-07-17": { busy: true, label: "fully booked" },
	};

	trip.addEventListener("change", (e) => {
		const { start, end, complete } = e.detail;
		if (complete) console.log(`${start}${end}`);
	});
</script>
<script lang="ts">
	import { Calendar } from "@xtyle/svelte";

	let value = $state("2026-07-14");
	let month = $state("2026-07");
</script>

<Calendar
	mode="single"
	{value}
	{month}
	min="2026-07-01"
	decorations={{ "2026-07-16": { dots: ["success"], label: "1 event" } }}
	isDateDisabled={(iso) => iso.endsWith("-25")}
	onchange={(e) => (value = e.detail.value)}
	onmonthchange={(e) => (month = e.detail.month)}
/>

<p>Picked: {value || "nothing yet"}</p>
---
import Calendar from "@xtyle/astro/Calendar.astro";
---

<!-- server-rendered to real markup: the month grid is there before any JS loads -->
<Calendar
	mode="range"
	value="2026-07-14,2026-07-19"
	month="2026-07"
	locale="fr-FR"
	weekNumbers
	fixedWeeks
	label="Dates du séjour"
/>