Calendar Heatmap
Show a day grid by week with intensity and the current streak, such as habit completion.
import { CalendarHeatmap, StreakCheck, StreakDay, StreakStats, StreakSummary } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use a calendar heatmap for anything done day by day, where the run matters as much as the total: a habit, a practice, a daily check-in. Each week is a column and each weekday a row, with the newest week at the inline end. A done dot is lit in the tone, stronger the more got done; a missed day is a recessed well; a day a freeze token covered is hatched and keeps the run alive; days still to come are empty rings. StreakSummary sits over it with the current run and how far it is to the next milestone, and StreakCheck is the "Mark done" button for a header or a list row. The Streaks block puts all three together. For a count per day without a run to keep, use a Chart; for one bounded value, a Segmented Meter.
import { StreakCheck, CalendarHeatmap, StreakSummary, streakStats, type StreakDay } from '@youj/react'
const [days, setDays] = useState<StreakDay[]>([
{ date: '2026-09-26' },
{ date: '2026-09-27', frozen: true },
])
const setToday = (done: boolean) =>
setDays((list) => [...list.filter((d) => d.date !== '2026-09-28'), ...(done ? [{ date: '2026-09-28' }] : [])])
<StreakCheck pressed={streakStats(days, '2026-09-28').today} onPressedChange={setToday} />
<StreakSummary days={days} today="2026-09-28" freezes={{ left: 2, total: 3 }} />
<CalendarHeatmap days={days} today="2026-09-28" onTodayChange={setToday} />A day is an ISO date, a value out of the grid's max (1 by default, so a listed day is done), and frozen for a day a freeze token covered. Days not listed are missed. With max above 1 the dots take four levels of heat, and the legend reads from Less to More; tone sets their color (the accent by default). weeks is how many columns show (26 by default) and weekStart whether a week starts on Monday (1, the default) or Sunday (0).
today is the newest day. Leave it out and the grid reads the browser's date once the page is live, and moves on at midnight. Give onTodayChange and today's dot becomes a toggle, called with whether today is now done; the grid is controlled, so update days there. size="sm" draws a mini grid for a list row: dots only, not interactive, and an image named by label if you give one.
StreakSummary takes the same days and today, and shows the current run, cells toward the next of its milestones (7, 30, 100 and 365 days by default), the longest run, the completion rate and, with freezes, the tokens left. streakStats(days, today) returns the same figures (current, longest, rate and whether today counts yet) for your own copy. StreakCheck is a toggle: pass pressed and onPressedChange, and label and doneLabel to change its words.
Description
A morning-run habit shown as a half-year grid of days, with a check for today.
Code
calendar-heatmap-demo.tsx, the Demo story. The component itself comes from the package.
Reference
Propsdays, today, weeks… (19)
Props
| Prop | Type | |
|---|---|---|
daysRequired | StreakDay[] | – |
today | stringThe newest day, as an ISO date. Default: today in the browser's time zone, once the page is live. | – |
weeks | numberWeeks shown, oldest first. default 26 | 26 |
max | numberThe value that fills a dot. default 1 | 1 |
tone | StreakTone'accent''success''warning''chart-1''chart-2''chart-3''chart-4''chart-5'Done dots' color. default 'accent' | 'accent' |
size | 'default' | 'sm''sm' is a mini grid for a list row: dots only, not interactive. default 'default' | 'default' |
weekStart | 0 | 1The first day of the week: 0 Sunday, 1 Monday. default 1 | 1 |
onTodayChange | (done: boolean) => voidMakes today's dot a toggle, called with whether today is now done. | – |
label | stringThe grid's accessible name. default "Daily history" | "Daily history" |
locale | stringDefault: the YoujProvider locale. | – |
Other props pass through: extends ComponentProps<'div'>. | ||
| Prop | Type | |
|---|---|---|
daysRequired | StreakDay[] | – |
today | stringThe newest day, as an ISO date. Default: today in the browser's time zone. | – |
freezes | { left: number; total: number }Freeze tokens: how many are left, of how many the period allows. | – |
milestones | number[]The runs the flame meter counts toward. default [7, 30, 100, 365] | [7, 30, 100, 365] |
locale | stringDefault: the YoujProvider locale. | – |
Other props pass through: extends ComponentProps<'div'>. | ||
| Prop | Type | |
|---|---|---|
size | 'sm' | 'default'default 'sm' | 'sm' |
label | stringThe label while not done. default "Mark done" | "Mark done" |
doneLabel | stringThe label once done. default "Done" | "Done" |
sound | booleanWith sound on, marking plays default true | true |
Other props pass through: extends TogglePrimitive.Props. | ||
StylingParts and data attributes
Styling
Parts: calendar-heatmap (with data-size), calendar-heatmap-body, calendar-heatmap-month, calendar-heatmap-weekday, calendar-heatmap-day (with data-state, data-level and data-today), calendar-heatmap-dot, calendar-heatmap-footer, calendar-heatmap-readout and calendar-heatmap-legend; streak-summary, streak-summary-value, streak-summary-flame-icon, streak-summary-milestone, streak-summary-meter, streak-summary-cell (with data-state), streak-summary-stats, streak-summary-stat and streak-summary-token (with data-state). StreakCheck is a button part with data-streak-check. The dots' color is --streak-tone, frozen days --streak-frozen, the summary's flame --streak-flame, and a mini grid's dot --streak-mini (5px).
MotionMark today done, today still open, summary first paint… · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Mark today donethe dot pops in with one ripple | spring.settle0.45s, bounce 0.3 | 0.45s, bounce 0.3 | |
| Today still openthe dot breathes once a beat, three times the token | duration.deliberate560ms | 560ms | |
| Summary first paintthe meter’s cells light from the tail of the run to its head | duration.deliberateease.standard560ms · cubic-bezier(0.2, 0, 0, 1) | 560mscubic-bezier(0.2, 0, 0, 1) | |
| Run growsthe new head cell flares once, and the flame grows toward the milestone | duration.slowease.exit340ms · cubic-bezier(0.4, 0, 1, 1) | 340mscubic-bezier(0.4, 0, 1, 1) | |
| Mark donethe pill morphs to Done and the check draws in | spring.snappy0.2s, bounce 0 | 0.2s, bounce 0 |
Reduced motion
Dots and cells light at once, nothing breathes or ripples, the flame holds still and the check is drawn.
Reduced motion
Today's dot lights without the pop or the ripple and doesn't breathe while open. The summary's cells light at once, the flame holds still, and the check in StreakCheck is drawn without its stroke.
AccessibilityKeyboard, ARIA, announcements
Accessibility
- The grid is a group named by
label(default: the labels' "Daily history"); each day is a button named by its date and state ("Sat, Sep 26, Done", "Sun, Sep 27, Frozen"). Days to come are disabled; past days arearia-disabled. - The days are one tab stop, starting on today. Up and down step a day, left and right a week (mirrored in RTL), Home goes to the first day and End to today.
- With
onTodayChange, today is a toggle witharia-pressed, named "Today, …, Mark done" whichever state it's in. Marking it announces the new run ("Done for today. Streak: 24 days") in a polite live region. - The readout under the grid, the legend and the summary's meter are hidden from screen readers; the summary's figures are a description list.
StreakCheckis a toggle witharia-pressed, named "Mark done" unless you pass a name.- Its words come from the labels (
calendarHeatmap,streakDay,streakDone,streakMissed,streakFrozen,streakPartial,streakToday,streakMarkDone,streakTodayDone,streakTodayUndone,streakCurrent,streakLongest,streakRate,streakFreezes,streakDays,streakFreezesLeft,streakMilestone,streakLessandstreakMore), translated by the language packs.