ChronousDocumentation

Examples

Month view

A month grid with weekday headings, capped lanes and a “+2 more” affordance.

A month

A month grid with weekday headings, three lanes of all-day bars per week and a “+1 more” button on the days where something did not fit. The same events as the week view — only the range changed.

Running here

The component

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

type EventData = { title: string }

const Calendar = createCalendarComponents<EventData>()

const LANE_HEIGHT = 26

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

const EVENTS: EventInput<EventData>[] = [
  {
    id: 'offsite',
    start: '2026-03-16',
    end: '2026-03-20',
    data: { title: 'Offsite' },
  },
  {
    id: 'freeze',
    start: '2026-03-17',
    end: '2026-03-19',
    data: { title: 'Code freeze' },
  },
  {
    id: 'audit',
    start: '2026-03-17',
    end: '2026-03-21',
    data: { title: 'Security audit' },
  },
  {
    id: 'retro',
    start: '2026-03-18',
    end: '2026-03-19',
    data: { title: 'Retro' },
  },
  {
    id: 'review',
    start: '2026-03-18T14:00',
    end: '2026-03-18T15:30',
    data: { title: 'Design review' },
  },
]

export const Month = (): JSX.Element => (
  <Calendar.Root
    className="calendar"
    range={RANGE}
    events={EVENTS}
    locale="en-GB"
  >
    <Calendar.MonthGrid className="month">
      {/* MonthGrid is a flex column, so the weekday row brings its own grid */}
      <div className="weekdays">
        <Calendar.MonthWeekdays className="weekday" />
      </div>

      <Calendar.MonthRows className="row" maxLanes={3} laneHeight={LANE_HEIGHT}>
        <Calendar.MonthDays className="day">
          {({ dayLabel, lanes, hiddenBars }) => (
            <>
              <span className="day-label">{dayLabel}</span>

              {/* the bars float above the cells, so reserve their height */}
              <div style={{ height: lanes * LANE_HEIGHT }} />

              <Calendar.MonthTimedEvents className="dot">
                {({ event }) => event.data?.title}
              </Calendar.MonthTimedEvents>

              {hiddenBars.length > 0 && (
                <button className="more" type="button">
                  +{hiddenBars.length} more
                </button>
              )}
            </>
          )}
        </Calendar.MonthDays>

        <Calendar.MonthAllDayEvents className="bar">
          {({ event }) => event.data?.title}
        </Calendar.MonthAllDayEvents>
      </Calendar.MonthRows>
    </Calendar.MonthGrid>
  </Calendar.Root>
)

The stylesheet

month.css
.month {
  /* seven readable columns, or a horizontal scroll rather than a squeeze */
  min-width: 700px;
  color: #d8e5ff;
}

.weekdays {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
}

.weekday {
  padding: 8px 0;
  text-align: center;
  font-size: 12px;
  color: #8d8da0;
}

.row {
  display: grid;
  grid-template-columns: repeat(7, minmax(0, 1fr));
  border-top: 1px solid rgba(255, 255, 255, 0.14);
}

.day {
  position: relative;
  min-height: 132px;
  padding: 4px;
  border-left: 1px solid rgba(255, 255, 255, 0.06);
}

.day[data-in-current-period='false'] {
  background: rgba(255, 255, 255, 0.02);
  color: #6b6b6b;
}

/* 28px is the component's own lanesTopOffset: the bars start below it */
.day-label {
  display: block;
  height: 28px;
  text-align: center;
  font-size: 12px;
  line-height: 28px;
}

.bar,
.dot {
  padding: 0 6px;
  font-size: 11px;
  line-height: 20px;
  color: #0a0722;
  background: #8aa9f9;
  border-radius: 4px;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* A bar is exactly laneHeight tall, inline — so the air between two lanes
   has to come out of that height rather than from a margin. */
.bar {
  box-sizing: border-box;
  border-top: 6px solid transparent;
  background-clip: padding-box;
}

.dot {
  margin-top: 4px;
  background: #aa90f8;
}

.more {
  margin-top: 4px;
  font-size: 11px;
  color: #8d8da0;
  background: none;
  border: none;
  cursor: pointer;
}

The spacer

The bars of a week float above the cells, so each cell reserves room for them with lanes * LANE_HEIGHT — where lanes counts only what was actually drawn. Everything below that spacer, timed events included, then flows underneath the bars instead of behind them. Layout & lanes explains where those numbers come from.