Work with calendar dates, build month grids, and read and format dates in someone's language.
@ui-sdk/core/date provides date arithmetic, month grids, localized labels, and parsing for calendars and date fields.
The helpers work with date strings such as "2026-10-16" and run without a DOM. Calendar and Date Picker use the same helpers.
Dates as Strings
Real Dates
Build a date with isoDate, and read its fields with dateParts. Months run from 1 to 12. Both functions return undefined when the date doesn't exist:
import { dateParts, isISODate, isoDate } from "@ui-sdk/core/date"
isoDate(2026, 10, 16) // "2026-10-16"
isoDate(2026, 2, 30) // undefined
dateParts("2026-10-16") // { year: 2026, month: 10, day: 16 }
isISODate("2026-02-30") // falseimport * as React from "react"
import { dateParts, isISODate, isoDate, toISODate } from "@ui-sdk/core/date"
import {
Field,
FieldControl,
FieldDescription,
FieldLabel,
} from "@ui-sdk/react/field"
import { useLog } from "./log"
const labels = { year: "Year", month: "Month", day: "Day" }
function Example() {
const log = useLog()
const [fields, setFields] = React.useState({
year: "2026",
month: "2",
day: "30",
})
const [picked, setPicked] = React.useState("2026-10-16")
const built = isoDate(
Number(fields.year),
Number(fields.month),
Number(fields.day)
)
const change = (next: typeof fields) => {
setFields(next)
const date = isoDate(
Number(next.year),
Number(next.month),
Number(next.day)
)
log(
`isoDate(${next.year}, ${next.month}, ${next.day}): ${date ?? "no such day"}`
)
}
const pick = (next: string) => {
setPicked(next)
const parts = isISODate(next) ? dateParts(next) : undefined
log(
parts
? `${next}: year ${parts.year}, month ${parts.month}, day ${parts.day}`
: "Pick a date"
)
}
return (
<div data-slot="stack">
<div data-slot="date-field-group">
{(["year", "month", "day"] as const).map((field) => (
<Field key={field} invalid={!built}>
<FieldLabel>{labels[field]}</FieldLabel>
<FieldControl
inputMode="numeric"
value={fields[field]}
onChange={(event) =>
change({ ...fields, [field]: event.target.value })
}
/>
</Field>
))}
</div>
<div data-slot="date-field-group">
<Field>
<FieldLabel>Picked date</FieldLabel>
<FieldControl
type="date"
value={picked}
onChange={(event) => pick(event.target.value)}
/>
</Field>
<button type="button" onClick={() => pick(toISODate(new Date())!)}>
Today
</button>
</div>
</div>
)
}
export { Example }These strings use YYYY-MM-DD, the same format as <input type="date">. Valid dates sort in date order, so comparisons such as start < end work directly.
To bring in an existing value, use toISODate. It accepts an ISO string, a Date, or an object with year, month, and day, including a Temporal.PlainDate:
import { toISODate } from "@ui-sdk/core/date"
toISODate({ year: 2026, month: 10, day: 16 }) // "2026-10-16"
toISODate(new Date()) // Today's date in the runtime's local time zone.A Date represents an instant, so this conversion uses its local year, month, and day. The resulting string has no time zone.
Arithmetic
Whole Days
For a three-night stay, add three days to check-in:
import { addDays, daysBetween } from "@ui-sdk/core/date"
addDays("2026-12-24", 3) // "2026-12-27"
daysBetween("2026-12-24", "2026-12-27") // 3Negative numbers move backward. The arithmetic uses whole days, so crossing a daylight saving change doesn't add or lose a day.
Use clampDate to keep a date inside a booking season. weekday returns 0 for Sunday through 6 for Saturday:
import { clampDate, weekday } from "@ui-sdk/core/date"
clampDate("2026-10-20", "2026-11-01", "2027-01-31") // "2026-11-01"
weekday("2026-12-24") // 4: Thursday.import * as React from "react"
import {
addDays,
clampDate,
daysBetween,
formatDate,
weekday,
} from "@ui-sdk/core/date"
import {
Field,
FieldControl,
FieldDescription,
FieldLabel,
} from "@ui-sdk/react/field"
import { useLog } from "./log"
const season = { min: "2026-11-01", max: "2027-01-31" }
const today = "2026-10-16"
const days = [
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday",
]
function stay(picked: string, nights: number) {
const checkIn = clampDate(picked || today, season.min, season.max)
return { checkIn, checkOut: addDays(checkIn, nights) }
}
function Example() {
const log = useLog()
const [picked, setPicked] = React.useState("2026-12-24")
const [nights, setNights] = React.useState(3)
const { checkIn, checkOut } = stay(picked, nights)
const change = (nextPicked: string, nextNights: number) => {
setPicked(nextPicked)
setNights(nextNights)
const next = stay(nextPicked, nextNights)
if (next.checkIn !== nextPicked) {
log(`Moved into the season: ${next.checkIn}`)
}
log(
`${days[weekday(next.checkIn)]} to ${days[weekday(next.checkOut)]}, ${daysBetween(today, next.checkIn)} days after ${formatDate(today, "en-US", { month: "short", day: "numeric" })}`
)
}
return (
<div data-slot="stack">
<div data-slot="date-field-group">
<Field>
<FieldLabel>Check-in</FieldLabel>
<FieldControl
type="date"
value={picked}
onChange={(event) => change(event.target.value, nights)}
/>
</Field>
<Field>
<FieldLabel>Nights</FieldLabel>
<FieldControl
type="number"
min={1}
max={14}
value={nights}
onChange={(event) =>
change(picked, Number(event.target.value) || 1)
}
/>
</Field>
</div>
<Field>
<FieldLabel>Check-out</FieldLabel>
<FieldControl type="date" readOnly value={checkOut} data-check-out />
<FieldDescription data-check-in>
Check-in {days[weekday(checkIn)]},{" "}
{formatDate(checkIn, "en-US", { month: "long", day: "numeric" })}
</FieldDescription>
</Field>
</div>
)
}
export { Example }Weeks and Months
Moving by months keeps the day when possible. January 31 becomes February 28 because February has no 31st:
import { addMonths, addYears } from "@ui-sdk/core/date"
addMonths("2026-01-31", 1) // "2026-02-28"
addYears("2028-02-29", 1) // "2029-02-28"For boundaries, use startOfWeek and endOfWeek, or startOfMonth and endOfMonth. The week helpers take the first weekday, with 0 for Sunday:
import {
daysInMonth,
endOfMonth,
isSameMonth,
startOfWeek,
} from "@ui-sdk/core/date"
startOfWeek("2026-10-16", 1) // "2026-10-12": Monday.
endOfMonth("2026-02-10") // "2026-02-28"
daysInMonth(2028, 2) // 29
isSameMonth("2026-09-28", "2026-10-04") // falseimport * as React from "react"
import {
addMonths,
addYears,
daysInMonth,
endOfMonth,
endOfWeek,
isSameMonth,
startOfMonth,
startOfWeek,
} from "@ui-sdk/core/date"
function Example() {
const [date, setDate] = React.useState("2026-01-31")
const [weekStartsOn, setWeekStartsOn] = React.useState(1)
const [year, month] = date.split("-").map(Number)
const week = {
start: startOfWeek(date, weekStartsOn),
end: endOfWeek(date, weekStartsOn),
}
const steps: [string, string, () => string][] = [
["month-back", "− Month", () => addMonths(date, -1)],
["month", "+ Month", () => addMonths(date, 1)],
["year", "+ Year", () => addYears(date, 1)],
]
return (
<div data-slot="stack">
<div data-slot="controls">
{steps.map(([value, text, next]) => (
<button
key={value}
type="button"
data-size="sm"
onClick={() => setDate(next())}
>
{text}
</button>
))}
<button
type="button"
data-size="sm"
onClick={() => setDate("2026-01-31")}
>
Reset
</button>
<select
aria-label="Weeks start on"
value={weekStartsOn}
onChange={(event) => setWeekStartsOn(Number(event.target.value))}
>
<option value={0}>Weeks start on Sunday</option>
<option value={1}>Weeks start on Monday</option>
<option value={6}>Weeks start on Saturday</option>
</select>
</div>
<p data-date data-slot="date-value">
{date}
</p>
<dl>
<dt>Week</dt>
<dd data-this-week>
{week.start} to {week.end}
{!isSameMonth(week.start, week.end) && ", across two months"}
</dd>
<dt>Month</dt>
<dd data-this-month>
{startOfMonth(date)} to {endOfMonth(date)}, {daysInMonth(year, month)}{" "}
days
</dd>
</dl>
</div>
)
}
export { Example }Month Grids
A Month to Pick From
monthGrid returns rows of seven ISO dates. Days from the neighboring months fill the edges. Choose the first weekday with weekStartsOn:
import { firstDayOfWeek, monthGrid } from "@ui-sdk/core/date"
const weeks = monthGrid("2026-10-01", {
weekStartsOn: firstDayOfWeek("de-DE"),
})
// The first week runs from "2026-09-28" to "2026-10-04".Render one row per week and one button per date. This example adds roving focus, using each date as a key. Its custom move adds one day for a horizontal arrow or seven for a vertical arrow. When focus reaches another month, the example renders that month.
import * as React from "react"
import { createRovingFocus, definePrimitive, selects } from "@ui-sdk/core"
import {
addDays,
addMonths,
firstDayOfWeek,
formatDate,
isSameMonth,
monthGrid,
startOfMonth,
weekdayNames,
type ISODate,
} from "@ui-sdk/core/date"
import { createParts } from "@ui-sdk/react/primitive"
type MonthOptions = {
defaultValue: ISODate
locale: string
}
type MonthState = {
value: ISODate
month: ISODate
focused: string | null
locale: string
}
type MonthRootState = {
title: string
weekdays: string[]
weeks: ISODate[][]
}
const steps: Record<string, number> = {
ArrowUp: -7,
ArrowDown: 7,
ArrowLeft: -1,
ArrowRight: 1,
}
const month = definePrimitive({
name: "month",
options: { defaultValue: "string", locale: "string" },
state: (options: MonthOptions): MonthState => ({
value: options.defaultValue,
month: startOfMonth(options.defaultValue),
focused: null,
locale: options.locale,
}),
sync: (options) => ({ locale: options.locale }),
api: (context) => {
const { store } = context
const roving = createRovingFocus<MonthState, { value: string }>(context, {
slot: "day",
key: "focused",
remember: false,
preferred: (day) => day.key === store.get().value,
claims: (state, day) => day.key === state.value,
move: (event, { from, rtl }) => {
const by = steps[event.key]
return by === undefined
? undefined
: addDays(from.key, rtl && Math.abs(by) === 1 ? -by : by)
},
})
let view: (MonthRootState & { month: ISODate; locale: string }) | undefined
const viewOf = (state: MonthState) => {
if (view?.month !== state.month || view.locale !== state.locale) {
const weekStartsOn = firstDayOfWeek(state.locale)
view = {
month: state.month,
locale: state.locale,
title: formatDate(state.month, state.locale, {
month: "long",
year: "numeric",
}),
weekdays: weekdayNames(state.locale, "short", weekStartsOn),
weeks: monthGrid(state.month, { weekStartsOn }),
}
}
return view
}
return {
roving,
viewOf,
select: (date: ISODate) => store.set({ value: date }),
turn: (by: number) =>
store.set({ month: addMonths(store.get().month, by) }),
keydown(event: KeyboardEvent, el: HTMLElement) {
const moved = roving.keydown(event, el)
if (moved && !isSameMonth(moved.key, store.get().month)) {
store.set({ month: startOfMonth(moved.key) })
}
},
}
},
slots: (slot) => ({
root: slot<{}, MonthRootState>({
state: (state, { api }) => {
const { title, weekdays, weeks } = api.viewOf(state)
return { title, weekdays, weeks }
},
}),
grid: slot({ attrs: () => ({ role: "grid" }) }),
day: slot<{ value: string }, { selected: boolean; outside: boolean }>({
props: { value: "string" },
selects: selects({ focused: ({ key }) => [key, null] }),
state: (state, { key }) => ({
selected: key === state.value,
outside: !isSameMonth(key, state.month),
}),
attrs: (state, part) => ({
type: "button",
role: "gridcell",
tabindex: part.api.roving.tabIndex(state, part),
"aria-selected": String(part.key === state.value),
"aria-label": formatDate(part.key, state.locale),
"data-selected": part.key === state.value ? "" : undefined,
"data-outside": isSameMonth(part.key, state.month) ? undefined : "",
}),
press: ({ api, key }) => api.select(key),
on: { keydown: (event, { el, api }) => api.keydown(event, el!) },
bind: (el, part) => part.api.roving.bind(el, part),
}),
previous: slot({
attrs: () => ({ type: "button" }),
press: ({ api }) => api.turn(-1),
}),
next: slot({
attrs: () => ({ type: "button" }),
press: ({ api }) => api.turn(1),
}),
}),
})
const parts = createParts(month, {
root: "div",
grid: "div",
day: "button",
previous: "button",
next: "button",
})
const Month = parts.Root
const MonthGrid = parts.Grid
const MonthDay = parts.Day
const MonthPrevious = parts.Previous
const MonthNext = parts.Next
function Example() {
const [locale, setLocale] = React.useState("en-US")
return (
<div data-slot="stack">
<select
aria-label="Locale"
value={locale}
onChange={(event) => setLocale(event.target.value)}
>
<option value="en-US">English (US)</option>
<option value="de-DE">Deutsch</option>
</select>
<Month defaultValue="2026-10-16" locale={locale}>
{({ title, weekdays, weeks }) => (
<>
<div data-slot="row">
<MonthPrevious aria-label="Previous month">‹</MonthPrevious>
<span aria-live="polite" data-slot="month-heading">
{title}
</span>
<MonthNext aria-label="Next month">›</MonthNext>
</div>
<MonthGrid aria-label={title}>
<div role="row" data-slot="month-row">
{weekdays.map((name) => (
<span key={name} role="columnheader">
{name}
</span>
))}
</div>
{weeks.map((week) => (
<div key={week[0]} role="row" data-slot="month-row">
{week.map((day) => (
<MonthDay key={day} value={day}>
{Number(day.slice(8))}
</MonthDay>
))}
</div>
))}
</MonthGrid>
</>
)}
</Month>
</div>
)
}
export {
Example,
month,
Month,
MonthDay,
MonthGrid,
MonthNext,
MonthPrevious,
type MonthOptions,
type MonthRootState,
type MonthState,
}Weeks That Fit the Month
A grid has six weeks by default, which keeps adjacent calendars the same height. Pass fixedWeeks: false to return only the weeks the month needs:
monthGrid("2026-02-01").length // 6
monthGrid("2026-02-01", { fixedWeeks: false }).length // 4import * as React from "react"
import { formatDate, isSameMonth, monthGrid } from "@ui-sdk/core/date"
const months = ["2026-02-01", "2026-03-01", "2026-08-01"]
function Example() {
const [fixedWeeks, setFixedWeeks] = React.useState(true)
return (
<div data-slot="stack">
<div data-slot="controls">
<label data-slot="label">
<input
type="checkbox"
checked={fixedWeeks}
onChange={(event) => setFixedWeeks(event.target.checked)}
/>
Six weeks each
</label>
</div>
<div data-slot="month-sizes-row">
{months.map((month) => (
<div key={month} data-month={month} data-slot="month-sizes-month">
<p>
{formatDate(month, "en-US", { month: "long", year: "numeric" })}
</p>
{monthGrid(month, { fixedWeeks }).map((week) => (
<div key={week[0]} data-week data-slot="month-sizes-week">
{week.map((day) => (
<span
key={day}
data-outside={isSameMonth(day, month) ? undefined : ""}
>
{Number(day.slice(8))}
</span>
))}
</div>
))}
</div>
))}
</div>
</div>
)
}
export { Example }Names and Labels
Names in Any Language
Pass a locale to format a date for the person reading it. Without one, these functions use the runtime's locale:
import { formatDate, formatDateRange } from "@ui-sdk/core/date"
formatDate("2026-10-16", "de-DE") // "Freitag, 16. Oktober 2026"
formatDate("2026-10-16", "en-US", { month: "long", year: "numeric" }) // "October 2026"
formatDateRange("2026-10-16", "2026-10-20", "en-US") // "Oct 16 – 20, 2026"formatDate takes Intl.DateTimeFormat options. Its default includes the weekday, day, month, and year, which makes a useful accessible label for a day button.
For a calendar's headings, get month and weekday names separately. Keep the weekday names in the same order as the grid:
import { firstDayOfWeek, monthNames, weekdayNames } from "@ui-sdk/core/date"
const firstDay = firstDayOfWeek("de-DE") // 1: Monday.
const weekdays = weekdayNames("de-DE", "short", firstDay)
const months = monthNames("de-DE")You can get the page's locale with pageLocale. firstDayOfWeek also respects an explicit week preference, such as "en-US-u-fw-mon" for Monday.
import * as React from "react"
import {
dateFormatter,
firstDayOfWeek,
formatDate,
formatDateRange,
formatDateToParts,
monthNames,
weekdayNames,
} from "@ui-sdk/core/date"
const locales = [
["en-US", "English (US)"],
["en-US-u-fw-mon", "English (US), weeks from Monday"],
["de-DE", "Deutsch"],
["fr-FR", "Français"],
["ja-JP", "日本語"],
["fa-IR", "فارسی"],
]
const date = "2026-10-16"
function Example() {
const [locale, setLocale] = React.useState("en-US")
const first = firstDayOfWeek(locale)
return (
<div data-slot="stack">
<div data-slot="controls">
<select
aria-label="Locale"
value={locale}
onChange={(event) => setLocale(event.target.value)}
>
{locales.map(([value, text]) => (
<option key={value} value={value}>
{text}
</option>
))}
</select>
</div>
<p data-label data-slot="date-label">
{formatDateToParts(date, locale, { month: "long", day: "numeric" }).map(
(part, index) => (
<span key={index} data-type={part.type}>
{part.value}
</span>
)
)}
</p>
<dl>
<dt>Long</dt>
<dd data-long>{formatDate(date, locale)}</dd>
<dt>Range</dt>
<dd data-range>{formatDateRange(date, "2026-10-20", locale)}</dd>
<dt>Week</dt>
<dd data-weekdays>{weekdayNames(locale, "short", first).join(" ")}</dd>
<dt>Months</dt>
<dd data-months>
{monthNames(locale, "short").slice(0, 4).join(", ")}, …
</dd>
<dt>Calendar</dt>
<dd data-calendar>
{dateFormatter(locale, { month: "long" }).resolvedOptions().calendar}
</dd>
</dl>
</div>
)
}
export { Example }For a label with separate styles, formatDateToParts returns each formatted piece and its type:
import { formatDateToParts } from "@ui-sdk/core/date"
const parts = formatDateToParts("2026-10-16", "en-US", {
month: "long",
day: "numeric",
})
// [{ type: "month", value: "October" }, { type: "literal", value: " " }, { type: "day", value: "16" }]These helpers use Intl and reuse its formatters. They keep month and day labels aligned with the Gregorian grid. A locale that uses different months, such as Persian, uses Gregorian labels; calendars that share those months, such as Thai Buddhist, can keep their own years. dateFormatter(locale, options) exposes the cached formatter when you need it directly.
Today
todayDate() returns today's date where the code runs. Pass a time zone when the date belongs to a particular place:
import { todayDate } from "@ui-sdk/core/date"
todayDate()
todayDate("Asia/Tokyo")import * as React from "react"
import { formatDate, todayDate } from "@ui-sdk/core/date"
const zones = [
["Pacific/Auckland", "Auckland"],
["Asia/Tokyo", "Tokyo"],
["Europe/London", "London"],
["America/Los_Angeles", "Los Angeles"],
]
function Example() {
const [mounted, setMounted] = React.useState(false)
React.useEffect(() => setMounted(true), [])
return (
<ul aria-label="Today" data-slot="list">
{zones.map(([zone, city]) => (
<li key={zone} data-slot="row">
{city}
<span data-slot="hint" data-zone={zone}>
{mounted
? formatDate(todayDate(zone), "en-US", { dateStyle: "medium" })
: ""}
</span>
</li>
))}
<li data-slot="row">
Here
<span data-slot="hint" data-zone="here">
{mounted
? formatDate(todayDate(), "en-US", { dateStyle: "medium" })
: ""}
</span>
</li>
</ul>
)
}
export { Example }For server rendering, pass the same time zone on the server and client, or wait until the component mounts to mark today.
Typing a Date
Read What People Type
parseDate reads the locale's date order and returns an ISO date. It returns undefined for incomplete or invalid input:
import { parseDate } from "@ui-sdk/core/date"
parseDate("10/16/2026", "en-US") // "2026-10-16"
parseDate("16.10.2026", "de-DE") // "2026-10-16"
parseDate("16 oct 2026", "fr") // "2026-10-16"
parseDate("Feb 30", "en-US") // undefinedDots, slashes, and spaces work as separators. The parser also accepts ISO dates, month names, locale digits, and digits without separators, such as "16102026" in German.
When the year is missing, the parser uses the reference date's year. That defaults to the current year. Pass referenceDate when the result should stay predictable:
parseDate("October 16", "en-US", { referenceDate: "2026-01-01" }) // "2026-10-16"
parseDate("16.10.70", "de-DE", { referenceDate: "2026-01-01" }) // "1970-10-16"
parseDate("16.10.70", "de-DE", { referenceDate: "2060-01-01" }) // "2070-10-16"Two-digit years fall in the 100-year window ending 20 years after the reference year.
import * as React from "react"
import { datePattern, formatDate, parseDate } from "@ui-sdk/core/date"
import {
Field,
FieldControl,
FieldDescription,
FieldLabel,
} from "@ui-sdk/react/field"
import { useLog } from "./log"
const locales = [
["en-US", "English (US)"],
["de-DE", "Deutsch"],
["fr-FR", "Français"],
["ar-EG", "العربية"],
]
function Example() {
const log = useLog()
const [locale, setLocale] = React.useState("en-US")
const [text, setText] = React.useState("10/16/26")
const [reference, setReference] = React.useState("2026-10-16")
const pattern = datePattern(locale)
const read = (next: {
locale?: string
text?: string
reference?: string
}) => {
const options = { locale, text, reference, ...next }
const date = parseDate(options.text, options.locale, {
referenceDate: options.reference,
})
log(
`“${options.text}”: ${date ? `${date}, ${formatDate(date, options.locale)}` : "not a date"}`
)
}
return (
<div data-slot="stack">
<div data-slot="controls">
<select
aria-label="Locale"
value={locale}
onChange={(event) => {
setLocale(event.target.value)
read({ locale: event.target.value })
}}
>
{locales.map(([value, label]) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
<input
type="date"
aria-label="Reference date"
value={reference}
onChange={(event) => {
const next = event.target.value || "2026-10-16"
setReference(next)
read({ reference: next })
}}
/>
</div>
<Field>
<FieldLabel>Date</FieldLabel>
<FieldControl
placeholder={pattern.placeholder}
value={text}
onChange={(event) => {
setText(event.target.value)
read({ text: event.target.value })
}}
/>
<FieldDescription data-pattern>
{pattern.fields.join(` ${pattern.separator} `)}
</FieldDescription>
</Field>
</div>
)
}
export { Example }Use datePattern for the input's placeholder and a hint showing the order:
import { datePattern } from "@ui-sdk/core/date"
const pattern = datePattern("de-DE")
pattern.fields // ["day", "month", "year"]
pattern.placeholder // "TT.MM.JJJJ"
pattern.separator // "."
// pattern.example is December 31 in the current year, written in this locale.API Reference
All of these functions come from @ui-sdk/core/date.
| You Want To | Use |
|---|---|
| Validate or convert a date | isoDate, isISODate, dateParts, toISODate |
| Move a date or measure a stay | addDays, daysBetween, weekday, clampDate |
| Move between months or find their boundaries | addMonths, addYears, startOfWeek, endOfWeek, startOfMonth, endOfMonth, daysInMonth, isSameMonth |
| Build rows for a calendar | monthGrid |
| Label dates and calendar headings | formatDate, formatDateRange, formatDateToParts, monthNames, weekdayNames, firstDayOfWeek, dateFormatter |
| Find today in a time zone | todayDate |
| Read typed dates or show their format | parseDate, datePattern |