A heatmap calendar places one activity cell per day and uses color intensity to show relative volume. Build a deterministic 84-day React calendar whose 7 rows represent weekdays and whose 12 columns represent weeks, then reveal exact counts through a shared status line.
Implement the interaction in App.tsx. The root App component receives no props; the starter already builds the fixed days array and renders the full calendar, legend, and live status region.
Less–More legend is visible, and the status reads Hover a day.6 contributions — day 45.i from 0 through 83, use level = (i * 3 + (i % 7)) % 5 and count = level * 2 + (i % 3). Build this array once outside App.cell l${level} so levels 0 through 4 map to the five existing CSS colors.grid-auto-flow: column place indices 0–6 in week one, 7–13 in week two, and so on.number | null with useState. Both onMouseEnter and onFocus set the same index, and the status is derived from it.aria-label; keep the result in the existing polite live region.You'll keep one active day index in React state and derive the live status from the fixed day array.
The calendar has 84 stable data points. Each point needs one of five colors, but only the day reached by pointer or keyboard changes the status line. The data and layout are pure derivations; the active index is the only changing fact.
Build the days once, then let CSS perform the spatial work. The first seven array entries fill the first column because the grid has seven explicit rows and flows by column. A day's computed level selects l0 through l4.
A tempting handler changes the rendered paragraph directly:
function showDay(day: Day) {
document.querySelector(".status")!.textContent =
`${day.count} contributions — day ${day.i}`;
}
That makes the DOM a second source of truth. React does not know which day is active and can replace the text on a later render. It also leaves keyboard focus without equivalent behavior unless you duplicate the mutation.
import { useState } from "react";
import "./styles.css";
type Day = { i: number; count: number; level: number };
function buildDays(): Day[] {
const days: Day[] = [];
for (let i = 0; i < 84; i++) {
const level = (i * 3 + (i % 7)) % 5;
const count = level * 2 + (i % 3);
days.push({ i, count, level });
}
return days;
}
const days = buildDays();
export default function App() {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const active = activeIndex === null ? null : days[activeIndex];
return (
<main className="container">
<h1>Heatmap Calendar</h1>
<div className="grid" role="group" aria-label="Activity by day">
{days.map((d) => (
<button
key={d.i}
type="button"
className={`cell l${d.level}`}
aria-label={`Day ${d.i}: ${d.count} contributions`}
onMouseEnter={() => setActiveIndex(d.i)}
onFocus={() => setActiveIndex(d.i)}
/>
))}
</div>
<div
className="legend"
role="group"
aria-label="Activity intensity: less to more"
>
<span>Less</span>
<span className="cell l0" aria-hidden="true" />
<span className="cell l1" aria-hidden="true" />
<span className="cell l2" aria-hidden="true" />
<span className="cell l3" aria-hidden="true" />
<span className="cell l4" aria-hidden="true" />
<span>More</span>
</div>
<p className="status" role="status" aria-live="polite">
{active
? `${active.count} contributions — day ${active.i}`
: "Hover a day"}
</p>
</main>
);
}
The module-level array stays stable across renders. activeIndex stores only the changing identity, while active and the status string are derived. Both input paths call the same setter, and native buttons provide keyboard focus without extra key handlers.
activeIndex is null, so the status is Hover a day.setActiveIndex(45) schedules a render.active becomes days[45] and the status announces 6 contributions — day 45.null explicitly.days in App — every hover would allocate 84 new objects. Build immutable data once at module scope.onFocus.grid-template-rows: repeat(7, 1fr).This version stores the selected day record directly and sends both input paths through a reducer. A small day button component preserves the exact markup while keeping event wiring close to each accessible control.
import { useReducer } from "react";
import "./styles.css";
type Day = { i: number; count: number; level: number };
const days: Day[] = Array.from({ length: 84 }, (_, i) => {
const level = (i * 3 + (i % 7)) % 5;
return { i, level, count: level * 2 + (i % 3) };
});
function selectedDayReducer(_current: Day | null, next: Day): Day {
return next;
}
function DayButton({ day, select }: { day: Day; select: (day: Day) => void }) {
return (
<button
type="button"
className={`cell l${day.level}`}
aria-label={`Day ${day.i}: ${day.count} contributions`}
onMouseEnter={() => select(day)}
onFocus={() => select(day)}
/>
);
}
export default function App() {
const [selectedDay, select] = useReducer(selectedDayReducer, null);
return (
<main className="container">
<h1>Heatmap Calendar</h1>
<div className="grid" role="group" aria-label="Activity by day">
{days.map((day) => (
<DayButton key={day.i} day={day} select={select} />
))}
</div>
<div className="legend" role="group" aria-label="Activity intensity: less to more">
<span>Less</span>
<span className="cell l0" aria-hidden="true" />
<span className="cell l1" aria-hidden="true" />
<span className="cell l2" aria-hidden="true" />
<span className="cell l3" aria-hidden="true" />
<span className="cell l4" aria-hidden="true" />
<span>More</span>
</div>
<p className="status" role="status" aria-live="polite">
{selectedDay
? `${selectedDay.count} contributions — day ${selectedDay.i}`
: "Hover a day"}
</p>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
A heatmap calendar places one activity cell per day and uses color intensity to show relative volume. Build a deterministic 84-day React calendar whose 7 rows represent weekdays and whose 12 columns represent weeks, then reveal exact counts through a shared status line.
Implement the interaction in App.tsx. The root App component receives no props; the starter already builds the fixed days array and renders the full calendar, legend, and live status region.
Less–More legend is visible, and the status reads Hover a day.6 contributions — day 45.i from 0 through 83, use level = (i * 3 + (i % 7)) % 5 and count = level * 2 + (i % 3). Build this array once outside App.cell l${level} so levels 0 through 4 map to the five existing CSS colors.grid-auto-flow: column place indices 0–6 in week one, 7–13 in week two, and so on.number | null with useState. Both onMouseEnter and onFocus set the same index, and the status is derived from it.aria-label; keep the result in the existing polite live region.You'll keep one active day index in React state and derive the live status from the fixed day array.
The calendar has 84 stable data points. Each point needs one of five colors, but only the day reached by pointer or keyboard changes the status line. The data and layout are pure derivations; the active index is the only changing fact.
Build the days once, then let CSS perform the spatial work. The first seven array entries fill the first column because the grid has seven explicit rows and flows by column. A day's computed level selects l0 through l4.
A tempting handler changes the rendered paragraph directly:
function showDay(day: Day) {
document.querySelector(".status")!.textContent =
`${day.count} contributions — day ${day.i}`;
}
That makes the DOM a second source of truth. React does not know which day is active and can replace the text on a later render. It also leaves keyboard focus without equivalent behavior unless you duplicate the mutation.
import { useState } from "react";
import "./styles.css";
type Day = { i: number; count: number; level: number };
function buildDays(): Day[] {
const days: Day[] = [];
for (let i = 0; i < 84; i++) {
const level = (i * 3 + (i % 7)) % 5;
const count = level * 2 + (i % 3);
days.push({ i, count, level });
}
return days;
}
const days = buildDays();
export default function App() {
const [activeIndex, setActiveIndex] = useState<number | null>(null);
const active = activeIndex === null ? null : days[activeIndex];
return (
<main className="container">
<h1>Heatmap Calendar</h1>
<div className="grid" role="group" aria-label="Activity by day">
{days.map((d) => (
<button
key={d.i}
type="button"
className={`cell l${d.level}`}
aria-label={`Day ${d.i}: ${d.count} contributions`}
onMouseEnter={() => setActiveIndex(d.i)}
onFocus={() => setActiveIndex(d.i)}
/>
))}
</div>
<div
className="legend"
role="group"
aria-label="Activity intensity: less to more"
>
<span>Less</span>
<span className="cell l0" aria-hidden="true" />
<span className="cell l1" aria-hidden="true" />
<span className="cell l2" aria-hidden="true" />
<span className="cell l3" aria-hidden="true" />
<span className="cell l4" aria-hidden="true" />
<span>More</span>
</div>
<p className="status" role="status" aria-live="polite">
{active
? `${active.count} contributions — day ${active.i}`
: "Hover a day"}
</p>
</main>
);
}
The module-level array stays stable across renders. activeIndex stores only the changing identity, while active and the status string are derived. Both input paths call the same setter, and native buttons provide keyboard focus without extra key handlers.
activeIndex is null, so the status is Hover a day.setActiveIndex(45) schedules a render.active becomes days[45] and the status announces 6 contributions — day 45.null explicitly.days in App — every hover would allocate 84 new objects. Build immutable data once at module scope.onFocus.grid-template-rows: repeat(7, 1fr).This version stores the selected day record directly and sends both input paths through a reducer. A small day button component preserves the exact markup while keeping event wiring close to each accessible control.
import { useReducer } from "react";
import "./styles.css";
type Day = { i: number; count: number; level: number };
const days: Day[] = Array.from({ length: 84 }, (_, i) => {
const level = (i * 3 + (i % 7)) % 5;
return { i, level, count: level * 2 + (i % 3) };
});
function selectedDayReducer(_current: Day | null, next: Day): Day {
return next;
}
function DayButton({ day, select }: { day: Day; select: (day: Day) => void }) {
return (
<button
type="button"
className={`cell l${day.level}`}
aria-label={`Day ${day.i}: ${day.count} contributions`}
onMouseEnter={() => select(day)}
onFocus={() => select(day)}
/>
);
}
export default function App() {
const [selectedDay, select] = useReducer(selectedDayReducer, null);
return (
<main className="container">
<h1>Heatmap Calendar</h1>
<div className="grid" role="group" aria-label="Activity by day">
{days.map((day) => (
<DayButton key={day.i} day={day} select={select} />
))}
</div>
<div className="legend" role="group" aria-label="Activity intensity: less to more">
<span>Less</span>
<span className="cell l0" aria-hidden="true" />
<span className="cell l1" aria-hidden="true" />
<span className="cell l2" aria-hidden="true" />
<span className="cell l3" aria-hidden="true" />
<span className="cell l4" aria-hidden="true" />
<span>More</span>
</div>
<p className="status" role="status" aria-live="polite">
{selectedDay
? `${selectedDay.count} contributions — day ${selectedDay.i}`
: "Hover a day"}
</p>
</main>
);
}Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.