Skip to content
txbrendan/ui

Heat Calendar

beta

Weeks across and days down, in one hue whose strength is how much happened, or each day in its event type's color, with a legend that filters and a span you can measure.

Preview

values

Training attendance

Arrow keys move a day or a week. Enter marks the start of a span, and again its end. Escape clears it.

events

Club calendar

Arrow keys move a day or a week. Enter marks the start of a span, and again its end. Escape clears it.

hue

Arrow keys move a day or a week. Enter marks the start of a span, and again its end. Escape clears it.

today

Arrow keys move a day or a week. Enter marks the start of a span, and again its end. Escape clears it.

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/heat-calendar

Use it for

  • Activity over months, like training attendance, logins, or match minutes, where the pattern matters more than one day.
  • Pass values as values[week][day] from 0 to 1, oldest week first and Monday first, with a title or aria-label.
  • Pass maxCount and unit, like 8 and 'sessions', so a day reads '4 sessions', not '50%'.
  • For a club's calendar, pass events: an ISO day to its event or events. Each day takes its type's color.
  • Pass types to name event types and set their order. Each color defaults to the next chart color.
  • Pass endDate for the last day shown. Without it the calendar ends today, once the page is in the browser.

Avoid it for

  • Exact numbers people must compare. Use BarChart or BreakdownList.
  • A trend over a few points. Use LineChart or Sparkline.
  • Booking or choosing a day. Use Calendar.
  • More than about five event types. The colors stop being easy to tell apart.

Props

  • values

    Intensity mode. values[week][day] from 0 to 1, Monday first. The last row is the week that holds endDate.

    number[][]

  • events

    Event mode. An ISO day, like '2026-09-26', to its event or events. An event is a type id, like 'training', or { type, label?, opponent?, score? }. A score of [24, 17] reads 'Won 24–17 vs Harbour Town'.

    Record<string, HeatEvent | HeatEvent[]>

  • types

    Event types in legend order. Default: each type in events as it first appears. A color is a theme token, like 'var(--chart-3)'.

    { id: string; label: string; color?: string }[]

  • weeks

    How many weeks to show.

    number · default values.length, or 26

  • maxCount

    The count a full cell stands for. With 8, a value of 0.5 reads '4 sessions'.

    number

  • unit

    The noun after counts, plural, like 'sessions'. A count of one drops the final s.

    string

  • endDate

    The last day shown, as an ISO day or a Date. One it cannot read falls back to today. Days after it in its week are left blank.

    Date | string · default today

  • title

    Names the calendar and its grid. Pass this, aria-label, or aria-labelledby.

    ReactNode

  • locale

    The locale for day and month names. The default keeps the server and the browser the same.

    string · default "en-US"

  • className

    Extra classes for the figure. '[--heat-calendar-color:var(--chart-3)]' sets the one hue in intensity mode. It defaults to --chart-1.

    string

Accessibility

  • The days are a role="grid", one row per weekday. Each day is a gridcell named by its date and what happened, like 'Sat, Sep 26: 4 sessions'.
  • The grid is one Tab stop. Left and Right move a week, Up and Down move a day, and Home and End go to the first and last days.
  • Enter or Space starts a span, and again ends it. A status line says the span's total. Escape clears it and goes no further.
  • The legend steps are buttons. Hovering or focusing one shows only its days. Pressing one keeps it on, with aria-pressed.
  • The readout over a day is hidden from screen readers, since each day already says the same.
  • In forced colors, the days, the legend swatches, and the readout's dots keep their hues, so each day still matches its legend step or type. Empty days and the None step mix the user's page and text colors, so they stay faint. The level steps and the event colors, including colors set in types, pull toward the text color. So on a light or a dark page, the steps stay in order above None, and event days stand out from the page. Words, outlines, and the focus ring take the user's colors.
  • With reduced motion, the cells fade in without the spring, and nothing lifts or glides.

Examples

Training attendance

<HeatCalendar
  title="Training attendance"
  values={weeks}
  maxCount={8}
  unit="sessions"
  endDate="2026-09-27"
/>

A club's calendar by event type

<HeatCalendar
  title="Club calendar"
  weeks={12}
  endDate="2026-09-27"
  types={[
    { id: "training", label: "Training" },
    { id: "match", label: "Match" },
  ]}
  events={{
    "2026-09-22": { type: "training", label: "Scrums and lineouts" },
    "2026-09-26": { type: "match", score: [24, 17], opponent: "Harbour Town" },
  }}
/>

In another hue

<HeatCalendar aria-label="Logins" values={weeks} className="[--heat-calendar-color:var(--chart-3)]" />