@dashforge/calendar-core is the headless engine behind the Dashforge date
picker suite. It is pure logic — a React hook and a set of dependency-free date
functions — with no UI and no styling of its own.
Both calendar skins build on it: the MUI <Calendar> / <DatePicker> in
@dashforge/ui and the Tailwind <Calendar> / <DatePicker> in @dashforge/tw
render the sameuseCalendar view-model with their own markup. If you need a
calendar that matches neither skin, consume the engine directly and render
whatever you like.
Isolation contract — this package is shared logic, never shared UI. It
has zero runtime dependencies (React is a peer). The view-model it returns is
render-agnostic: you map it to your own elements.
Installation
bash
npm install @dashforge/calendar-core
React 18 or 19 is a peer dependency. If you already use @dashforge/ui or
@dashforge/tw, the package is installed for you as a transitive dependency —
import from it directly when you need the headless API.
useCalendar
The hook turns a set of options into a render-ready month-grid view-model plus
action callbacks. It holds the displayed month and the roving-focus cell as
internal state — both can be controlled.
Every cell in weeks arrives with its render flags pre-computed — isSelected,
isToday, isDisabled, isSiblingMonth, isFocused — so the skin never has
to re-derive them.
Options — UseCalendarOptions
Option
Type
Default
Description
month / year
number
—
Controlled displayed month (1-indexed) + year.
defaultMonth / defaultYear
number
today
Uncontrolled initial displayed month + year.
selected
ISODate | ISODate[] | null
null
Selected date(s) — drives the isSelected flag.
focusedDate
ISODate
—
Controlled roving-focus cell.
defaultFocusedDate
ISODate
—
Uncontrolled initial roving-focus cell.
minDate / maxDate
ISODate
—
Selectable range bounds (inclusive).
disabledDates
ISODate[]
—
Explicit disabled dates.
isDateDisabled
(date: ISODate) => boolean
—
Predicate disabling arbitrary dates.
weekStartDay
WeekDay
0
First-column weekday (0 = Sunday).
locale
string
'en-US'
BCP-47 locale for month / weekday names.
today
ISODate
host date
Overrides "today" — mainly for testing.
onMonthChange
(month, year) => void
—
Fired when the displayed month changes.
onSelect
(date: ISODate) => void
—
Fired when a day is selected.
onFocusedDateChange
(date: ISODate) => void
—
Fired when roving focus moves.
Result — UseCalendarResult
Member
Type
Description
month / year
number
The displayed month (1-indexed) + year.
weeks
CalendarWeek[]
The 6-row month grid, each cell pre-flagged.
weekdayNames
string[]
Localized column headers, ordered from weekStartDay.
monthLabel
string
Localized "month year" label, e.g. "May 2026".
focusedDate
ISODate
The current roving-focus cell.
isDayDisabled
(date: ISODate) => boolean
True when a date is outside the selectable range.
goToMonth
(month, year) => void
Jumps the grid to a specific month / year.
goToNextMonth / goToPreviousMonth
() => void
Steps the displayed month.
goToNextYear / goToPreviousYear
() => void
Steps the displayed year.
selectDate
(date: ISODate) => void
Selects a date — a no-op if disabled.
setFocusedDate
(date: ISODate) => void
Moves the roving-focus cell.
navigate
(direction: ArrowDirection) => void
Moves focus one cell; follows the grid across month edges.
useDateRange
useDateRange builds on useCalendar to add range selection — a
{ start, end } pair. It layers a small state machine on the month grid: the
first selectDate sets the range start, the second sets the end (auto-swapped
if it lands earlier), a third restarts. While only a start is picked,
hoverDate drives a live preview band. It renders one or two consecutive
months (monthCount), so a skin can show a single calendar or a side-by-side
dual-month range picker.
Each cell extends CalendarDay with four range flags — isRangeStart,
isRangeEnd, isInRange (the committed band) and isRangePreview (the
hovered band).
Options — UseDateRangeOptions
Option
Type
Default
Description
value
DateRange
—
Controlled range.
defaultValue
DateRange
empty
Uncontrolled initial range.
onChange
(range: DateRange) => void
—
Fired on each change — partial on the first click, complete on the second.
monthCount
1 | 2
1
How many consecutive months to render.
month / year
number
—
Controlled displayed month (1-indexed) + year of the leading calendar.
defaultMonth / defaultYear
number
today
Uncontrolled initial displayed month + year.
onMonthChange
(month, year) => void
—
Fired when the displayed month changes.
minDate / maxDate
ISODate
—
Selectable range bounds (inclusive).
disabledDates
ISODate[]
—
Explicit disabled dates.
isDateDisabled
(date: ISODate) => boolean
—
Predicate disabling arbitrary dates.
weekStartDay
WeekDay
0
First-column weekday.
locale
string
'en-US'
BCP-47 locale.
today
ISODate
host date
Overrides "today".
Result — UseDateRangeResult
Member
Type
Description
months
CalendarRangeMonth[]
The rendered months — length equals monthCount.
weekdayNames
string[]
Localized column headers.
month / year
number
The leading calendar's displayed month + year.
monthLabel
string
Localized label of the leading month.
range
DateRange
The current { start, end } range.
focusedDate
ISODate
The current roving-focus cell.
isDayDisabled
(date: ISODate) => boolean
True when a date is outside the selectable range.
goToMonth
(month, year) => void
Jumps the leading calendar to a month / year.
goToNextMonth / goToPreviousMonth
() => void
Step the displayed month.
goToNextYear / goToPreviousYear
() => void
Step the displayed year.
selectDate
(date: ISODate) => void
Advances the range state machine — start, then end.
hoverDate
(date: ISODate | null) => void
Sets the preview-band hover cell (null clears it).
setFocusedDate
(date: ISODate) => void
Moves the roving-focus cell.
navigate
(direction: ArrowDirection) => void
Moves focus one cell; follows the grid across month edges.
Pure date utilities
The core/ layer is a flat set of pure, dependency-free functions. They take
and return ISODate strings (YYYY-MM-DD) or DateParts objects — never
JavaScript Date instances at the boundary — so there are no time-zone or
mutation surprises.
ISO parsing & formatting
Function
Signature
Description
toISODate
(parts: DateParts) => ISODate
Formats numeric parts as YYYY-MM-DD.
parseISODate
(value: string) => DateParts | null
Parses an ISO date; null if malformed.
isValidISODate
(value: string) => boolean
True for a well-formed, real calendar date.
Arithmetic
Function
Signature
Description
addDays
(parts: DateParts, amount: number) => DateParts
Shifts a date by N days.
addMonths
(parts: DateParts, amount: number) => DateParts
Shifts a date by N months.
getDaysInMonth
(year: number, month: number) => number
Day count of a month.
isLeapYear
(year: number) => boolean
Leap-year test.
getWeekday
(parts: DateParts) => WeekDay
Weekday of a date (0 = Sunday).
getTodayParts
() => DateParts
The host's local date as parts.
Comparison
Function
Signature
Description
compareISODate
(a, b) => number
< 0 / 0 / > 0 — sort-friendly.
isSameISODate
(a, b) => boolean
Equality.
isBeforeISODate / isAfterISODate
(a, b) => boolean
Strict ordering.
isWithinISORange
(value, start, end) => boolean
Inclusive range test.
getTodayISODate
() => ISODate
The host's local date as an ISO string.
Month grid
Member
Signature
Description
buildMonthGrid
(year, month, weekStartDay) => DateParts[]
The 42-cell grid for a month, including sibling days.
chunkIntoWeeks
(cells: T[]) => T[][]
Splits a flat cell list into week rows.
DAYS_IN_WEEK
7
Constant.
WEEKS_IN_GRID
6
Constant.
Localization
Function
Signature
Description
getMonthNames
(locale, width?) => string[]
Localized month names.
getWeekdayNames
(locale, weekStartDay) => string[]
Localized weekday names, ordered.
formatMonthYear
(parts, locale) => string
e.g. "May 2026".
formatLongDate
(parts, locale) => string
e.g. "May 20, 2026".
Time
The time helpers back the upcoming TimePicker — strings are HH:mm /
HH:mm:ss, never Date objects.
Function
Signature
Description
parseTimeString
(input: string) => string | null
Normalizes loose time input.
timeStringToMinutes
(value: string) => number | null
Time → minutes since midnight.
minutesToTimeString
(totalMinutes: number) => string
Minutes → HH:mm.
generateTimeOptions
(config?) => string[]
A list of time slots at a given step.
formatTime
(value, options?) => string
Localized time display.
Keyboard
Function
Signature
Description
resolveCalendarKey
(key: string) => CalendarKeyAction | null
Maps a KeyboardEvent.key to a grid action.
Types
Type
Description
ISODate
An ISO 8601 calendar date string — YYYY-MM-DD. No time, no zone.
A single day cell with all render flags pre-computed.
CalendarWeek
A week row — { key, days }.
CalendarKeyAction
The action a key resolves to — arrow / select / month step.
DateRange
A { start, end } pair — either side may be null.
CalendarRangeDay
A CalendarDay plus the four range flags.
CalendarRangeWeek
A week row of CalendarRangeDay.
CalendarRangeMonth
A rendered month — { month, year, monthLabel, weeks }.
Notes
Zero dependencies. The package ships pure TypeScript; React is the only
peer. Nothing else is pulled into your bundle.
No Date at the boundary. Inputs and outputs are ISODate strings or
DateParts. Date.UTC is used only internally for arithmetic, so a stored
YYYY-MM-DD never drifts across time zones or DST.
Headless by design.useCalendar renders nothing. For a ready-made UI
use <Calendar> / <DatePicker> from @dashforge/ui or @dashforge/tw;
reach for the hook only when you need a bespoke presentation.