GrCalendar
Overview
When to take it
- the calendar is the screen — a booking page, a planner, a day-selection panel: the grid has to be open rather than opened by a click in a field;
- the cells have content of their own — the
dayslot draws the load, the price, the number of events over a day; - the grid is needed next to something else — a panel of your own, buttons of your own, a footer
of your own through
headerandfooter; - the choice of a day is not related to a form — the value goes into the state of the screen rather than into an input field.
When to take something else
| Need | Take |
|---|---|
| A date as the value of a form field | GrDatePicker |
| A “from — to” period | GrDateRangePicker |
| A date with a time | GrDateTimePicker |
| A time only | GrTimePicker |
| Events in time as a feed rather than as a grid | GrTimeline |
There is no event calendar here. The grid shows the days rather than what happens in them: a
week and a day layout, dragging events, overlaps and a feed of resources are a separate domain, and
a separate package will cover it. The day slot covers only the marks on a day — a counter, a dot,
a price.
Examples 4 each with a live demo and its code
An appointment: free days and free times
minandmaxkeep the choice within six weeks from tomorrow, anddisabled-datestakes a predicate — a full day is a rule, not a list. Thedayslot marks the days that still have room; thefooterslot getsselectfrom the grid, so «First free day» cannot pick what the grid forbids.changeresets the time when the day changes.Book a check-upDr. Ines Novak · Harbour Street Practice October 2026Mon Tue Wed Thu Fri Sat Sun 28 29 30 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 1 2 3 4 5 6 7 8 Pick a dayDays with a green dot still have free times.Two months, a stay and a price per night
The grid only draws the range:
range-start,range-endandrange-previewcome from the screen, which decides what the second click means and follows the pointer throughday-hover. Two calendars share oneview-date, and theheaderslot puts the arrows on the outer edges. The range speaks as a whole, soannounce-selectionis off and the summary line is the live region;size="sm"keeps two months side by side.Harbour loft · HamburgPrices per night, weekends included October 2026Mon Tue Wed Thu Fri Sat Sun 28 29 30 1 2 3 4 5 6 89 7 89 8 89 9 119 10 119 11 89 12 89 13 89 14 89 15 89 16 119 17 119 18 89 19 89 20 89 21 89 22 89 23 24 25 89 26 89 27 89 28 89 29 89 30 119 31 119 1 2 3 4 5 6 7 8 November 2026Mon Tue Wed Thu Fri Sat Sun 26 27 28 29 30 31 1 89 2 89 3 89 4 89 5 89 6 119 7 119 8 89 9 89 10 89 11 89 12 89 13 119 14 119 15 89 16 89 17 89 18 89 19 89 20 119 21 119 22 89 23 89 24 89 25 89 26 89 27 119 28 119 29 89 30 89 1 2 3 4 5 6 Check-in 16 Oct — now the check-out day.A report period: week, month, quarter or year
modeturns the grid into weeks, months, quarters or years, and the model is always the first day of the period. Week numbers help in week mode;period-changereports the year being shown. While the report builds, the grid isdisabled, and a new kind of period returns the keyboard to it throughfocus().Revenue reportSeptember 2026Week Mon Tue Wed Thu Fri Sat Sun 36 31 1 2 3 4 5 6 37 7 8 9 10 11 12 13 38 14 15 16 17 18 19 20 39 21 22 23 24 25 26 27 40 28 29 30 1 2 3 4 41 5 6 7 8 9 10 11 Period28 Sept 2026 – 4 Oct 2026Only finished and current periodsAn on-call rota of separate days
selected-datesdraws a set, and the screen adds and removes days onchange.locale="en-US"starts the week on Sunday — the locale decides that, not the screen — and theweekdayslot marks the weekend columns. A published rota isreadonly: readable and focusable, but fixed.size="sm"keeps the grid compact next to the summary; «Today» callsfocusDate().On-call · Grace WuSupport team, New YorkOctober 2026Sun Mon Tue Wed Thu Fri Sat 27 28 29 30 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 1 2 3 4 5 6 7 6 days on call · 4 at weekendsNextThu, Oct 8Wed, Oct 14Sat, Oct 17
API generated from the package · cannot drift
Props
| Prop | Type | default | Description |
|---|---|---|---|
mode | "day" | "week" | PeriodMode | undefined | "day" | What gets selected: a day, a month or a year. In period modes the grid shows twelve cells in three columns, and the selection returns the first day of the period. |
modelValuev-model | PlainDate | null | undefined | null | Selected date. `null` — nothing is selected. |
viewDatev-model | PlainDate | undefined | undefined | The month being shown. Without it the calendar keeps track of it itself, starting from the selection. |
min | PlainDate | undefined | undefined | The earliest date that can be selected. Days before it are visible but disabled, and paging past its month is not possible. |
max | PlainDate | undefined | undefined | The latest date that can be selected. Days after it are visible but disabled, and paging past its month is not possible. |
disabledDates | readonly PlainDate[] | ((date: PlainDate) => boolean) | undefined | undefined | Disabled dates: a list or a predicate. A list is normalized into a `Set` once. |
weekStart | IsoWeekday | undefined | undefined | First day of the week per ISO (1 — Monday). Not set — taken from the locale via `Intl`. |
showWeekNumbers | boolean | undefined | undefined | A column of ISO 8601 week numbers to the left of the grid. Not set — from `GrConfigProvider`, otherwise `false`. |
today | PlainDate | undefined | undefined | What counts as today. Set for reproducible tests and snapshots. |
locale | string | undefined | undefined | Display locale. Not set — from the application i18n adapter. |
size | "xs" | "sm" | "md" | "lg" | undefined | undefined | The size step: the cells, the type size and the paging arrows. `md` by default; taken from `GrConfigProvider`. |
disabled | boolean | undefined | false | The calendar is unavailable: selection and paging are switched off, the grid is marked `aria-disabled`. |
readonly | boolean | undefined | false | The selection is fixed: the selected date is visible and read out by a screen reader but does not change, and paging is stopped too. The grid is marked `aria-readonly`. |
ariaLabel | string | undefined | undefined | Accessible name for the grid when there is no label nearby. |
rangeStart | PlainDate | null | undefined | null | Start of the selected range. The grid decides nothing about the range — it only draws it: computing the bounds, the order and the length limits is the picker job. |
rangeEnd | PlainDate | null | undefined | null | End of the selected range. |
rangePreview | PlainDate | null | undefined | null | The second edge of the preview while the range is still open: the day under the cursor or under the keyboard stop. |
announceSelection | boolean | undefined | true | Announce the selection in a live region. Turned off when the shell announces it: for a range it is the period state that makes sense ("start selected"), not a single day, and two announcements on one click would talk over each other. |
selectedDates | readonly PlainDate[] | undefined | — | The set of selected dates. As with the range, the grid decides nothing about it — it only draws it: adding to the set, removing from it and sorting it is the picker job. |
Slots
| Slot | Type | Description |
|---|---|---|
day | { cell: CalendarCell; selected: boolean; } | A custom day cell instead of the number. |
header | { title: string; goToPeriod: (delta: number) => void; } | A custom header instead of the title and the arrows. |
weekday | { label: string; full: string; isoWeekday: IsoWeekday; } | A custom weekday header cell instead of the abbreviated day name. |
footer |
| Panel footer: "today" and "clear" buttons, preset periods. The selection is passed inside because it cannot be reproduced from outside: a disabled date adds up from `min`, `max` and `disabledDates`, and `readonly` forbids any selection at all. |
Events
| Event | Signature | Description |
|---|---|---|
update:modelValue | (event: "update:modelValue", value: PlainDate): void | — |
update:viewDate | (event: "update:viewDate", value: PlainDate): void | — |
change | (event: "change", value: PlainDate): void | — |
periodChange | (event: "periodChange", value: PlainDate): void | — |
dayHover | (event: "dayHover", value: PlainDate | null): void | — |
Methods
| Methods | Signature | Description |
|---|---|---|
focus | () => void | Put focus on the cell that holds the keyboard stop of the grid. |
goToPeriod | (delta: number) => void | Page the view by `delta` steps: plus — forward, minus — back. A step is a month, in the `month` and `year` modes — a year and a decade; it does not page with `disabled`, `readonly` or beyond `min`/`max`. |
focusDate | (date: PlainDate) => void | Move the view to the month with this date and put focus on it. |
Types
Every exported type of the subpath, grouped by the file it lives in. Names, generics and bodies come from the package’s declaration files and cannot drift.
import type { GrCalendarConfigurableProps, GrCalendarSize, GrCalendarProps } from '@feugene/granularity-chrono/components/GrCalendar'Filedist/types/components/GrCalendar/config.d.ts
const grCalendarConfig: import("@feugene/granum/contract").GranumComponentDescriptor<"GrCalendar">;Filedist/types/components/GrCalendar/defaults.d.ts
interface GrCalendarConfigurableProps {
size: GrCalendarSize;
showWeekNumbers: boolean;
weekStart: IsoWeekday;
}Filedist/types/components/GrCalendar/grCalendarStyles.d.ts
type GrCalendarSize = GrComponentSize;Filedist/types/components/GrCalendar/safelist.d.ts
const grCalendarSafelist: string[];Filedist/types/components/GrCalendar/GrCalendar.vue.d.ts
interface GrCalendarProps {
mode?: 'day' | 'week' | PeriodMode;
modelValue?: PlainDate | null;
viewDate?: PlainDate;
min?: PlainDate;
max?: PlainDate;
disabledDates?: DisabledDatesInput;
weekStart?: IsoWeekday;
showWeekNumbers?: boolean;
today?: PlainDate;
locale?: string;
size?: GrCalendarSize;
disabled?: boolean;
readonly?: boolean;
ariaLabel?: string;
rangeStart?: PlainDate | null;
rangeEnd?: PlainDate | null;
rangePreview?: PlainDate | null;
selectedDates?: readonly PlainDate[];
announceSelection?: boolean;
}interface GrCalendarEmits {
(e: 'update:modelValue', value: PlainDate): void;
(e: 'update:viewDate', value: PlainDate): void;
(e: 'change', value: PlainDate): void;
(e: 'periodChange', value: PlainDate): void;
(e: 'dayHover', value: PlainDate | null): void;
}Usage from the library prose
The model is a PlainDate rather than a Date
The only component of the package whose internal { y, m, d } tuple goes outside rather than a
Date through a valueAdapter. The reason is that a calendar is not a form control: it has nothing
to serialise, and a Date at the boundary would mean midnight in some zone — exactly the ambiguity
for the sake of avoiding which the package computes on tuples
(../model.md).
If the value is needed as a Date, it is assembled on your own side; the pickers do the same, only
inside themselves.
The first day of the week comes from the locale rather than from a prop
Monday in Russia and Sunday in the USA is Intl rather than a setting of the component. The
locale prop changes both the names and the order of the days at once; changing them separately is
not allowed, otherwise a calendar comes out that does not exist in any country.
When the locale does have to be overridden — for instance the working week starts on Sunday
regardless of the language of the interface — that is a decision of the application rather than of a
screen. Such a thing is set once through GrConfigProvider:
<GrConfigProvider :component-defaults="{ GrCalendar: { weekStart: 7, showWeekNumbers: true } }">The setting goes under the GrCalendar key and reaches all of the pickers: their panel is shared,
and GrDatePicker and its neighbours deliberately have no key of their own. The resolution order is
the usual one — the prop in place is stronger than the config, and the config is stronger than the
locale; if nothing is set, Intl decides.
The grid draws the selection but does not compute it
A range arrives in the grid as two props (rangeStart, rangeEnd) and a set as one
(selectedDates), and the grid changes neither of them: adding, removing, sorting and checking the
length are the business of the picker. The calendar answers one question: how to colour a cell.
Because of that it serves equally well GrDatePicker with its set, GrDateRangePicker with its
segment, and an application that runs the selection itself.
Membership of the set is computed with a Set assembled once per change of the set: walking the
array for each of the forty-two cells would turn the highlighting into a quadratic.
Five modes, and the week among them is not a grid of periods
mode | What it shows | What it puts into the model |
|---|---|---|
day (the default) | a grid of days | the selected day |
week | the same grid of days | the beginning of the selected week |
month | twelve months | the first day of the month |
quarter | four quarters | the first day of the quarter |
year | the twelve years of a decade | the first of January |
A week is drawn as a grid of days rather than as a grid of periods. Twelve cells in three columns are twelve weeks on the screen, a quarter of a year without a single month label: there is nothing to choose in such a grid. Instead a click on any day selects the week it fell into, and the row is highlighted as a whole.
The beginning of the week is taken from the locale (see “The first day of the week” above) rather
than nailed to Monday: in the USA the same date falls into a week starting on Sunday. One date is
put into the model — the beginning of the week — and the shape of the value stays shared across all
of the modes: valueAdapter works as it worked.
The package does not show week numbers: ISO and the USA count them differently, and Intl does not
provide it — the package does not undertake to invent something locale-dependent past Intl.
There are four quarters, and their grid has two columns — three would leave a lonely cell in the
second row. The label is taken from a string of the package locale (Q1, 1 qtr): Intl does not
name quarters at all, and this is interface text rather than locale-dependent data.
A deterministic render requires today
The clock of the environment is read once per instance. Where the render is obliged to match a past
one — snapshots, tests, server rendering — “today” is set with the today prop: otherwise the first
client render will diverge from the server one exactly at midnight. The details —
../ssr.md.
Props in this example