ChronousDocumentation

Examples

Week view

A full week with an all-day strip, a time grid, a now marker and a toolbar.

A week

A week with a toolbar, day headings, an all-day strip that never collapses, a time grid scrolled to eight in the morning and a recurring standup. This is the real component running on this page — the arrows step it, and the two blocks below are the file and the stylesheet that produced it.

Running here

The component

Week.tsx
import { createCalendarComponents } from '@midstem/chronous-react'
import type { CalendarRange, EventInput } from '@midstem/chronous-react'
import { useState } from 'react'

type EventData = { title: string }

const Calendar = createCalendarComponents<EventData>()

const INITIAL: CalendarRange = {
  view: 'week',
  currentDate: '2026-03-18',
  timeZone: 'Europe/Kyiv',
}

const EVENTS: EventInput<EventData>[] = [
  {
    id: 'offsite',
    start: '2026-03-18',
    end: '2026-03-20',
    data: { title: 'Offsite' },
  },
  {
    id: 'standup',
    start: '2026-03-16T09:00',
    duration: 'PT15M',
    recurrence: { rule: 'FREQ=WEEKLY;BYDAY=MO,WE,FR' },
    data: { title: 'Standup' },
  },
  {
    id: 'review',
    start: '2026-03-18T14:00',
    end: '2026-03-18T15:30',
    data: { title: 'Design review' },
  },
  {
    id: 'pairing',
    start: '2026-03-18T14:30',
    end: '2026-03-18T16:00',
    data: { title: 'Pairing' },
  },
]

export const Week = (): JSX.Element => {
  const [range, setRange] = useState<CalendarRange>(INITIAL)

  return (
    <Calendar.Root
      className="calendar"
      range={range}
      events={EVENTS}
      locale="en-GB"
      gutterWidth="56px"
    >
      <Calendar.Toolbar className="toolbar" onNavigate={setRange}>
        {({ title, navigation, goTo }) => (
          <>
            <button
              type="button"
              disabled={!navigation.prev}
              onClick={() => navigation.prev && goTo(navigation.prev)}
            >
              ←
            </button>
            <strong>{title}</strong>
            <button
              type="button"
              disabled={!navigation.next}
              onClick={() => navigation.next && goTo(navigation.next)}
            >
              →
            </button>
          </>
        )}
      </Calendar.Toolbar>

      <Calendar.Header className="header">
        <Calendar.DayHeadings className="heading">
          {({ weekdayLabel, dayLabel }) => (
            <>
              <span>{weekdayLabel}</span> <strong>{dayLabel}</strong>
            </>
          )}
        </Calendar.DayHeadings>
      </Calendar.Header>

      <Calendar.AllDayRow
        className="all-day"
        minLanes={1}
        gutterCell={<span className="gutter-label">all-day</span>}
      >
        <Calendar.AllDayEvents className="bar">
          {({ event }) => event.data?.title}
        </Calendar.AllDayEvents>
      </Calendar.AllDayRow>

      <Calendar.TimeGrid className="grid" hourHeight={48} scrollToHour={8}>
        <Calendar.TimeAxis>
          <Calendar.TimeLabels className="tick" />
        </Calendar.TimeAxis>

        <Calendar.DayColumns className="column">
          <Calendar.TimeSlots className="line" />
          <Calendar.NowMarker className="now" />
          <Calendar.TimedEvents as="button" className="event">
            {({ event, box }) => (
              <>
                {event.data?.title}
                {box.continuesAfter && ' ↓'}
              </>
            )}
          </Calendar.TimedEvents>
        </Calendar.DayColumns>
      </Calendar.TimeGrid>
    </Calendar.Root>
  )
}

The stylesheet

Nothing here positions anything: the grid columns, the height of a box and the lane of a bar all arrive as inline styles. What is left is colour, borders and type.

week.css
.calendar {
  display: flex;
  flex-direction: column;
  /* seven readable columns, or a horizontal scroll rather than a squeeze */
  min-width: 660px;
  height: 640px;
  color: #d8e5ff;
  background: #020013;
  border: 1px solid rgba(255, 255, 255, 0.14);
  border-radius: 12px;
  overflow: hidden;
}

.toolbar {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 10px 12px;
}

.header,
.all-day {
  border-bottom: 1px solid rgba(255, 255, 255, 0.14);
}

.heading {
  padding: 8px 0;
  text-align: center;
  font-size: 13px;
  border-left: 1px solid rgba(255, 255, 255, 0.06);
}

.all-day {
  padding: 4px 0;
  background: rgba(255, 255, 255, 0.02);
}

/* The gutter label belongs to the axis, not to the row: same size, same
   colour and the same right edge as the hour ticks under it. */
.gutter-label {
  display: block;
  padding-right: 8px;
  color: #8d8da0;
  font-size: 10px;
  line-height: 24px;
  text-align: right;
}

/* A time label is centred on its own line, so half of the first one sits
   above the grid. The padding is the room it hangs into — without it the
   all-day strip clips 00:00. */
.grid {
  flex: 1;
  padding-top: 10px;
}

.tick {
  right: 8px;
  font-size: 10px;
  color: #8d8da0;
}

.column {
  border-left: 1px solid rgba(255, 255, 255, 0.06);
}

.line {
  border-top: 1px solid rgba(255, 255, 255, 0.06);
}

.now {
  border-top: 2px solid #e67a56;
}

.event,
.bar {
  padding: 0 6px;
  font-size: 11px;
  text-align: left;
  color: #0a0722;
  background: #aa90f8;
  border: none;
  border-radius: 6px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

.bar {
  background: #8aa9f9;
  line-height: 24px;
}

Worth noting

  • gutterWidth sits on the root because the header, the all-day row and the grid share one column template.
  • minLanes of 1 holds the all-day strip open on weeks that have no all-day event, so the grid below it does not jump as you step. Try the arrows above.
  • as="button" makes each event a real button — focusable, and keyboard-operable — without changing anything else.
  • The standup is one event with a rule, expanded only for the week on screen. See Recurrence.
  • NowMarker draws only in the column that is today, so it stays hidden while the range is anchored on March 2026.