Guides
Labels
formatIso reads either shape a calendar hands back, so a day heading can never slip a day.
One formatter, either shape
No formatting ships with the engine and the hooks take no locale. Labels are yours, and formatIso turns a string a calendar hands back into one without asking which of the two shapes you are holding.
tsx
import { buildCalendar, formatIso } from '@midstem/chronous' const calendar = buildCalendar( { view: 'day', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv' }, [{ id: 'standup', start: '2026-03-18T09:00', duration: 'PT30M' }], ) const [day] = calendar.days const [box] = day.boxes // a bare date: formatted as the floating date it is console.log( formatIso(day.date, { locale: 'uk-UA', options: { day: 'numeric', month: 'long' } }), ) // '18 березня' // a date-time: read in the offset it already carries console.log( formatIso(box.start, { locale: 'en-GB', options: { hour: '2-digit', minute: '2-digit' } }), ) // '09:00' // the same instant, shown somewhere else console.log( formatIso(box.start, { locale: 'en-GB', timeZone: 'America/New_York', options: { timeStyle: 'short' }, }), ) // '03:00'
Options
type FormatOptions = { locale: LocaleId timeZone?: TimeZoneId options?: DateTimeFormatOptions }
Dates and moments
- A date —
2026-03-18— is formatted as the floating date it is and is never moved into a zone, so a day heading cannot slip a day.timeZoneis ignored for it. - A date-time carries its offset, which fixes the instant.
timeZonedecides the zone it is shown in and defaults to the offset the string already has, soslot.startreads back as the wall time the row stands for. - A date-time with no offset is floating too, and reads as written.
Floating
import { formatIso } from '@midstem/chronous' // A date is never moved into a zone, so a day heading cannot slip a day. console.log( formatIso('2026-03-18', { locale: 'en-GB', timeZone: 'America/New_York', options: { dateStyle: 'full' }, }), ) // 'Wednesday, 18 March 2026' — timeZone is ignored for a bare date // This is what new Date() gets wrong west of Greenwich: console.log(new Date('2026-03-18').toLocaleDateString('en-US', { timeZone: 'America/New_York', dateStyle: 'full', })) // 'Tuesday, March 17, 2026'
The labels you already have
Inside the components most labels are formatted for you, in the locale given to Calendar.Root. Anything ending in Label is a string ready to render.
tsx
import { Calendar } from '@midstem/chronous-react' import type { CalendarRange } from '@midstem/chronous-react' const range: CalendarRange = { view: 'week', currentDate: '2026-03-18', timeZone: 'Europe/Kyiv', } // locale on Root is what every built-in label is formatted in. export const Headings = () => ( <Calendar.Root range={range} events={[]} locale="uk-UA"> <Calendar.Header> <Calendar.DayHeadings className="heading"> {({ weekdayLabel, dayLabel }) => ( <> <span>{weekdayLabel}</span> <strong>{dayLabel}</strong> </> )} </Calendar.DayHeadings> </Calendar.Header> </Calendar.Root> )
Formatters are cached, so a month grid formatting forty-two cells on every render builds one formatter, not forty-two.