component

Calendar

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.

Keyboard

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.

Default

August 2026
SMTWTFS
---
import Calendar from "../calendar.astro";
import { stack } from "@pindoba/styled-system/patterns";
---

<div class={stack({ gap: "md" })}>
  <Calendar id="astro-demo-calendar-default" />
</div>

Range

Set range to true to let users pick a start and end date. While selecting, hovering a cell previews the range.

August 2026
SMTWTFS
---
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.)

Month range

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).

2026
---
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>

Year range

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).

2020 – 2029
---
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>

Multiple months

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.

August 2026
SMTWTFS
September 2026
SMTWTFS
---
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>

Restricted views

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.

Month + year picker

2026
---
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>

Standalone month picker

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.

2026
---
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>

Disabled dates & highlights

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.

Disablingdisabled={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.

Highlightinghighlights 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.

December 2026
SMTWTFS
---
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>

Month & year pickers

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.

2026
---
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>
props · 35 shown · 35 total
actions svelte
CalendarActionsnull

Imperative actions (prev/next/today/reset/setView/goToDate). Bindable.

background
"surface.peak""surface.hill""surface.base""surface.valley""surface.ground""transparent"
default "surface.peak"

Panel background token (inherited from Panel).

border
"none""bold""default""muted""accent"
default "default"

Panel border token (inherited from Panel).

disabled
DateDate[]{ from: Date; to?: Date; }{ dayOfWeek: number[]; }SerializableCalendarDateMatcher[]

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)`.

displayDate svelte
Date

Bindable current display date (the month/year shown in the header).

element binding svelte
HTMLDivElementnull

Bindable element reference for the container div.

emphasis
"primary""secondary""tertiary"

Panel emphasis variant (inherited from Panel).

feedback
"primary""neutral""success""warning""danger""inherit"
default "neutral"

Color palette (inherited from Panel).

fullWidth
boolean
default false

Stretch the calendar to fill its container. By default it sizes to its content.

highlights
CalendarHighlight[]

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.

id
nullstring

Stable identifier for the calendar root element.

initialDate
Date
default new Date()

Initial display + focus date. Does not select the date.

initialView
"days""months""years"

Which view to show on mount. Falls back to the leaf view if the value isn't included in `views`.

locale
string

BCP 47 locale tag used for label formatting and the first day of the week.

maxValue
Date

Latest selectable date (inclusive). Later days render disabled.

minValue
Date

Earliest selectable date (inclusive). Earlier days render disabled.

numberOfMonths
number
default 1

Number 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.

onDisplayDateChange svelte
(date: Date) => void

Fires when the displayed month/year changes (navigation, today, etc.).

onSelectionChange svelte
(selection: CalendarSelection) => void

Fires whenever the selected date or range changes.

onViewChange svelte
(view: CalendarView) => void

Fires when the active view changes (days/months/years).

padding
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""7xl""8xl""4xs""3xs""2xs""9xl""10xl""11xl"
default "sm"

Panel padding token (inherited from Panel).

passThrough
CalendarPassThrough<RootElementAttributes>

Per-slot style/attribute override bag — inject Panda styles or extra HTML attributes onto any rendered slot.

radius
"sm""md""lg""xl""2xl""none""xs""3xl""4xl""5xl""6xl""full""2xs""inner""inherit"
default "lg"

Panel radius token (inherited from Panel).

range
boolean
default false

Enable start/end range selection. The first click sets the start, the second the end (sorted automatically); hovering previews the range.

selection svelte
CalendarSelection

Bindable current selection. `null` when nothing is selected.

shadow
"sm""md""lg""xl""none""xs"
default "none"

Panel shadow token (inherited from Panel).

showControls
boolean
default true

Render the Pindoba header (view switcher + prev/next). Set to false to drive navigation entirely from the actions API.

showFooter
boolean
default true

Render the footer row (Today / Reset).

showReset
boolean
default false

Show a Reset button in the footer (clears the selection).

showSelectionLabel
boolean
default false

Render the formatted current selection above the grid (e.g. "May 9, 2026"). Reserves vertical space so the grid never shifts.

showToday
boolean
default true

Show a Today button in the footer.

size
"sm""md""lg""xs"
default "md"

Visual size variant — controls font size and minimum height.

translucent
boolean

Render the panel surface with a translucent background (inherited from Panel).

view svelte
CalendarView

Bindable current view (`"days" | "months" | "years"`).

views
readonly CalendarView[]
default ["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.

Type

  • Components
  • Blocks