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:
DayKey, WeekKey, MonthKey).firstDayOfWeek for layout.date-fns), shipped as ESM + CJS.npm install date-grid
# or
yarn add date-grid
# or
pnpm add date-grid
import {
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 render
buildRange → groupByMonth | groupByWeek → fillAdjacentDays → splitRows → addWeekNumbers
Date[] Section[] Section[] Section[] Section[]
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. |
Item = Cell | MarkerA 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 WeekKey
firstDayOfWeekWeek semantics are split on purpose:
addWeekNumbers and groupByWeek always use ISO weeks (getISOWeek, getISOWeekYear), regardless of locale.firstDayOfWeek option (0 | 1 | 6) only affects layout — where fillAdjacentDays pads and how splitRows aligns 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, toWeekKeyExported types:
Range, Section, Row, Item, Cell, MarkerDayKey, WeekKey, MonthKey, FirstDayOfWeekDecorator, FillAdjacentDaysOptions, GroupByWeekOptions, WeekNumberMetayarn build # tsc + vite lib build → dist/
yarn test # vitest run
MIT