ChronousDocumentation

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. timeZone is ignored for it.
  • A date-time carries its offset, which fixes the instant. timeZone decides the zone it is shown in and defaults to the offset the string already has, so slot.start reads 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.