A framework-agnostic date layout engine for building calendar-like interfaces.
date-grid turns a flat array of Dates into a structured, week-aligned grid. It does not render anything — you map the output to your own UI (React, Vue, Svelte, plain DOM, terminal, PDF, anywhere).
Most calendar libraries ship a renderer you have to fight. date-grid only does the layout math:
- Composable, pure-function pipeline — pick the stages you need.
- Zero UI assumptions — works in any framework, any runtime.
- Strongly typed with branded keys (
DayKey,WeekKey,MonthKey). - ISO weeks for numbering, configurable
firstDayOfWeekfor layout. - Tiny: one runtime dependency (
date-fns), shipped as ESM + CJS.
npm install date-grid
# or
yarn add date-grid
# or
pnpm add date-gridimport {
addWeekNumbers,
buildRange,
fillAdjacentDays,
groupByMonth,
splitRows,
} from 'date-grid';
const dates = buildRange({
start: new Date('2026-03-01T00:00:00.000Z'),
end: new Date('2026-03-31T00:00:00.000Z'),
});
const sections = addWeekNumbers(
splitRows(
fillAdjacentDays(groupByMonth(dates), { firstDayOfWeek: 1 }),
),
);
// sections[0].rows -> 6 rows of 7 items, ready to renderflowchart LR
A["buildRange<br/><code>Date[]</code>"] --> B["groupByMonth | groupByWeek<br/><code>Section[]</code>"]
B --> C["fillAdjacentDays<br/><code>Section[]</code>"]
C --> D["splitRows<br/><code>Section[]</code>"]
D --> E["addWeekNumbers<br/><code>Section[]</code>"]
Each stage is a pure function that returns a new array. Stages are composable and order-dependent.
| Stage | Purpose |
|---|---|
buildRange({ start, end }) |
Generate an inclusive Date[] between two dates. |
groupByMonth(dates) |
Bucket dates into one Section per calendar month. |
groupByWeek(dates, opts?) |
Bucket dates into one Section per ISO week. |
fillAdjacentDays(sections, opts?) |
Pad each section's edges with previous/next month days (inMonth: false) so it starts and ends on a week boundary. |
splitRows(sections) |
Split section.items into 7-day section.rows. |
addWeekNumbers(sections) |
Inject a Marker at the start of each row carrying ISO week metadata. |
A section's items is a flat stream of two kinds of entries:
Cell— a real day:{ kind: 'day', date, dayKey, inMonth }.Marker— metadata-only entry (e.g. a week-number badge):{ kind: 'marker', id, meta? }.
Decorators inject markers into the same stream rather than maintaining a parallel array. Markers always sort to the start of a row.
DayKey, WeekKey, and MonthKey are nominal string types. Build them through the helpers — never hand-format strings, or TypeScript will reject them at the boundary:
import { toDayKey, toMonthKey, toWeekKey } from 'date-grid';
toMonthKey(new Date('2026-03-15')); // '2026-03' as MonthKey
toDayKey(new Date('2026-03-15')); // '2026-03-15' as DayKey
toWeekKey(new Date('2026-03-15')); // ISO week key as WeekKeyWeek semantics are split on purpose:
addWeekNumbersandgroupByWeekalways use ISO weeks (getISOWeek,getISOWeekYear), regardless of locale.- The
firstDayOfWeekoption (0 | 1 | 6) only affects layout — wherefillAdjacentDayspads and howsplitRowsaligns 7-day rows.
ISO week numbers do not change when firstDayOfWeek changes.
The Decorator type is the contract for new pipeline stages:
import type { Decorator } from 'date-grid';
const highlightToday: Decorator = (sections) =>
sections.map((s) => ({
...s,
items: s.items.map((item) =>
item.kind === 'day' && isToday(item.date)
? { ...item, /* attach your own meta via a Marker if needed */ }
: item,
),
}));Compose it like any other stage — highlightToday(addWeekNumbers(...)).
Exported values:
buildRange,groupByMonth,groupByWeek,fillAdjacentDays,splitRows,addWeekNumberstoDayKey,toMonthKey,toWeekKey
Exported types:
Range,Section,Row,Item,Cell,MarkerDayKey,WeekKey,MonthKey,FirstDayOfWeekDecorator,FillAdjacentDaysOptions,GroupByWeekOptions,WeekNumberMeta
yarn build # tsc + vite lib build → dist/
yarn test # vitest runMIT