component
An accessible date picker built entirely from Pindoba components. A Choice segmented control switches between Day / Month / Year views; a navigation row shows the current period (“July 2024”) with prev/next Buttons; and the day grid is a real WAI-ARIA grid (role="grid" with roving-tabindex day cells styled as Buttons). All date math — month layout, week start, range selection, and keyboard navigation — runs through @internationalized/date, so locales, time zones, and leap years are handled correctly. The surface (background, border, radius, shadow, padding) delegates to Panel, so the same design tokens and variants apply here.
By default the calendar sizes to its content; set fullWidth to stretch it to the container.
Selection state lives in a single pure reducer. On Svelte it’s held in a rune; on Astro it’s driven by a small boot script that re-renders the grid on each change — no third-party calendar library is involved.
The grid follows the standard date-picker pattern: arrow keys move the focused day (±1 day / ±1 week), Home/End jump to the week edges, PageUp/PageDown step a month (hold Shift for a year), and Enter/Space select. In the months and years views the arrows move by month/year and PageUp/PageDown step a year/decade.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
---
import Calendar from "../calendar.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md" })}>
<Calendar id="astro-demo-calendar-default" />
</div>Set range to true to let users pick a start and end date. While selecting, hovering a cell previews the range.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
---
import Calendar from "../calendar.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md" })}>
<Calendar id="astro-demo-calendar-range" showReset range />
</div>Range selection works at the leaf view’s granularity, and the leaf needs the coarser views for navigation context — so in range mode views is expanded to be downward-closed: a month range gets the years view, a day range gets months + years. (Single-select keeps standalone ['months']/['years'] pickers.)
A ['months', 'years'] picker with range selects a month range — pick a start and end month, using the years view to move between years. The committed value is inclusive of the whole end month (March–July commits 2026-03-01 … 2026-07-31).
---
import Calendar from "../calendar.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md" })}>
<Calendar
id="astro-demo-calendar-month-range"
views={["months", "years"]}
range
showReset
/>
</div>A standalone ['years'] picker with range selects a year range, inclusive of the whole end year (2024–2027 commits 2024-01-01 … 2027-12-31).
---
import Calendar from "../calendar.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md" })}>
<Calendar
id="astro-demo-calendar-year-range"
views={["years"]}
range
showReset
/>
</div>Set numberOfMonths (1–3) to render several month grids side by side. Keyboard navigation and range selection span the whole window; only the first month carries prev/next.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
---
import Calendar from "../calendar.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md" })}>
<Calendar id="astro-demo-calendar-multi-month" range numberOfMonths={2} />
</div>Pass views to choose which views are available. The lowest-granularity entry is the leaf view — clicking a cell at the leaf emits a selection (first-of-clicked-month for a months leaf). In a non-leaf view, clicking a cell drills down toward the leaf.
---
import Calendar from "../calendar.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md" })}>
<Calendar
id="astro-demo-calendar-month-year"
showReset
views={["months", "years"]}
/>
</div>A single-view views={['months']} (or ['years']) renders a true month-only (or year-only) picker — no day grid, no view tabs — selecting at that granularity.
---
import Calendar from "../calendar.astro";
import { stack } from "@pindoba/styled-system/patterns";
---
<div class={stack({ gap: "md" })}>
<Calendar
id="astro-demo-calendar-month-picker"
views={["months"]}
showReset
/>
</div>Beyond the minValue/maxValue window, you can disable dates and tint them with semantic color. Both use one matching vocabulary — a DateMatcher — so you learn it once:
type DateMatcher =
| Date // one date
| Date[] // specific dates
| { from: Date; to?: Date } // an inclusive span (omit `to` for open-ended)
| { dayOfWeek: number[] } // weekdays, 0 = Sunday
| ((date: Date) => boolean); // a predicate (Svelte only — can't serialize)
Pass one matcher or an array (matches if any matches). The month(year, month) and year(year) helpers (exported from the package) return whole-period span matchers — month(2026, 7) matches all of July.
Matching is granularity-aware: in a day picker a matcher hits days, in a month-only picker it hits whole months, in a year-only picker whole years (a date inside July matches the July cell). { dayOfWeek } only applies to day cells. In a full day calendar the month/year views are pure navigation, governed by minValue/maxValue alone.
Disabling — disabled={matcher}. OR-combined with the minValue/maxValue bounds. Keyboard navigation skips disabled cells — arrow keys land on the next available date, and the cursor never rests on (nor does Enter commit) a disabled date.
Highlighting — highlights is a list of { match, feedback, emphasis?, label? } rules. match is a DateMatcher; feedback is a semantic palette (neutral/primary/success/warning/danger); emphasis is:
subtle (default) recolors just the text — quietly communicate (“this is a holiday”, weekends).strong adds a tinted surface chip — draw attention.The first matching rule wins, and an optional label is appended to the cell’s aria-label. Highlights stay visually distinct from the selected state (solid fill) and today (accent ring) — a selected day always keeps its selection styling.
Astro note: predicate (function) matchers can’t be serialized, so on Astro they only apply to the server render, not to client-side navigation. Use the declarative forms (dates, spans,
{ dayOfWeek },month()/year()) on Astro; the function form is available on Svelte.
| S | M | T | W | T | F | S |
|---|---|---|---|---|---|---|
---
import Calendar from "../calendar.astro";
import { stack } from "@pindoba/styled-system/patterns";
// Anchor on December so the seasonal highlights are visible on mount.
const december = new Date(2026, 11, 1);
---
<div class={stack({ gap: "md" })}>
<Calendar
id="astro-demo-calendar-holidays"
initialDate={december}
disabled={[new Date(2026, 11, 28), new Date(2026, 11, 29)]}
highlights={[
{
match: new Date(2026, 11, 25),
feedback: "danger",
emphasis: "strong",
label: "Christmas Day",
},
{
match: [new Date(2026, 11, 24), new Date(2026, 11, 31)],
feedback: "warning",
emphasis: "subtle",
label: "Holiday",
},
{ match: { dayOfWeek: [0, 6] }, feedback: "neutral", emphasis: "subtle" },
]}
/>
</div>In a month-only (views={['months']}) or year-only (views={['years']}) picker, the same disabled/highlights matchers hit whole months or years — reach for the month(2026, 7) / year(2025) helpers, or pass any date inside the period. The prev/next chevrons also disable automatically once minValue/maxValue (or fully-disabled matchers) leave no selectable period in that direction.
---
import Calendar from "../calendar.astro";
import { month } from "@pindoba/core-calendar";
import { stack } from "@pindoba/styled-system/patterns";
// A standalone month picker (months is the leaf). Matchers apply at month
// granularity — the `month()` helper makes "highlight a whole month" explicit.
// min/max bound the window: months before April read as disabled (earliest
// selectable month), and the year nav disables once there's no selectable
// month left either way.
const april = new Date(2026, 3, 1);
---
<div class={stack({ gap: "md" })}>
<Calendar
id="astro-demo-calendar-month-picker-rules"
views={["months"]}
initialDate={april}
minValue={april}
maxValue={new Date(2026, 11, 31)}
highlights={[
{
match: [month(2026, 6), month(2026, 7), month(2026, 8)],
feedback: "warning",
emphasis: "subtle",
label: "Summer",
},
{
match: month(2026, 12),
feedback: "success",
emphasis: "strong",
label: "Launch",
},
]}
/>
</div>Imperative actions (prev/next/today/reset/setView/goToDate). Bindable.
"surface.peak"Panel background token (inherited from Panel).
"default"Panel border token (inherited from Panel).
Dates to disable (un-selectable), in addition to the `minValue`/`maxValue` window. Accepts any {@link CalendarDateMatcher} (or an array): a date, a span, `{ dayOfWeek: [0, 6] }` for weekends, or a helper like `month(2026, 7)`.
Bindable current display date (the month/year shown in the header).
Bindable element reference for the container div.
Panel emphasis variant (inherited from Panel).
"neutral"Color palette (inherited from Panel).
falseStretch the calendar to fill its container. By default it sizes to its content.
Highlight rules — tint matched dates with a semantic color and intensity (e.g. holidays `strong`, weekends `subtle`). Each rule's `match` is a {@link CalendarDateMatcher}; rules are evaluated in order, first match wins.
Stable identifier for the calendar root element.
new Date()Initial display + focus date. Does not select the date.
Which view to show on mount. Falls back to the leaf view if the value isn't included in `views`.
BCP 47 locale tag used for label formatting and the first day of the week.
Latest selectable date (inclusive). Later days render disabled.
Earliest selectable date (inclusive). Earlier days render disabled.
1Number of month grids to render side by side in the day view (clamped 1–3). Range selection and keyboard navigation span them. Ignored in month/year views.
Fires when the displayed month/year changes (navigation, today, etc.).
Fires whenever the selected date or range changes.
Fires when the active view changes (days/months/years).
"sm"Panel padding token (inherited from Panel).
Per-slot style/attribute override bag — inject Panda styles or extra HTML attributes onto any rendered slot.
"lg"Panel radius token (inherited from Panel).
falseEnable start/end range selection. The first click sets the start, the second the end (sorted automatically); hovering previews the range.
Bindable current selection. `null` when nothing is selected.
"none"Panel shadow token (inherited from Panel).
trueRender the Pindoba header (view switcher + prev/next). Set to false to drive navigation entirely from the actions API.
trueRender the footer row (Today / Reset).
falseShow a Reset button in the footer (clears the selection).
falseRender the formatted current selection above the grid (e.g. "May 9, 2026"). Reserves vertical space so the grid never shifts.
trueShow a Today button in the footer.
"md"Visual size variant — controls font size and minimum height.
Render the panel surface with a translucent background (inherited from Panel).
Bindable current view (`"days" | "months" | "years"`).
["days", "months", "years"]Which views the user can navigate between. The lowest-granularity entry (`days` < `months` < `years`) becomes the leaf — clicking a cell at the leaf emits a selection (first-of-month for `months`, January 1st for `years`). Pass a single-entry array to render an unswitchable picker.
Plus all standard <div> HTML
attributes.