Most Active Days
Calendar activity-ring card.
Calendar
Activity rings on a calendar.
Most active days
32,459
total stepsJanuaryJanuary
Jan
Feb
Mar
Apr
May
Jun
Jul
Aug
Sep
Oct
Nov
Dec
function MostActiveDaysDemo() {
return <MostActiveDaysCard totalSteps={MOST_ACTIVE_DAYS_TOTAL_STEPS} />;
}function MostActiveDaysDemo() {
return <MostActiveDaysCard totalSteps={MOST_ACTIVE_DAYS_TOTAL_STEPS} />;
}Installation
npx shadcn@latest add https://boardcn.dev/r/most-active-days-card.jsonnpx shadcn@latest add https://boardcn.dev/r/most-active-days-card.jsonBoardCN dependencies
The CLI installs these for you — you do not need to add them yourself.
Source
The file the CLI copies into your project.
"use client";
import { useEffect, useRef, useState } from "react";
import { WeekRangePill } from "@/components/blocks/medical/week-range-pill";
import {
ACTIVITY_MONTHS,
ACTIVITY_YEAR,
activityRingPct,
} from "@/components/charts/activity-rings-card";
import type { ActivityDay } from "@/components/charts/activity-rings-card";
import { cx } from "@/utils/cx";
const MINI_RINGS = [
{ radius: 12, color: "var(--color-chart-3-active)" },
{ radius: 8.4, color: "var(--color-chart-2)" },
{ radius: 4.8, color: "var(--color-chart-4)" },
] as const;
function MiniActivityRings({ month, day }: ActivityDay) {
// The BoardCN preview represents dates after its July 10 checkpoint as
// future days. Their tracks remain visible, but progress is intentionally
// omitted.
const isFutureDay = month > 6 || (month === 6 && day > 10);
return (
<svg viewBox="0 0 28 28" className="size-7 shrink-0" aria-hidden="true">
{MINI_RINGS.map((ring, index) => {
const circumference = 2 * Math.PI * ring.radius;
const progress = circumference * activityRingPct(month, day, index);
return (
<g key={ring.radius} transform="rotate(-90 14 14)">
<circle
cx={14}
cy={14}
r={ring.radius}
fill="none"
stroke={ring.color}
strokeWidth={2.2}
opacity={0.16}
/>
{!isFutureDay && (
<circle
cx={14}
cy={14}
r={ring.radius}
fill="none"
stroke={ring.color}
strokeWidth={2.2}
strokeLinecap="round"
strokeDasharray={`${progress} ${circumference - progress}`}
/>
)}
</g>
);
})}
</svg>
);
}
export type MostActiveDaysCardProps = {
/** Headline total shown above the calendar. */
totalSteps: number;
/** Currently selected calendar date. */
selectedDay?: ActivityDay | null;
/** Called when a day is selected. Pair with ActivityRingsCard to update it. */
onSelectDay?: (day: ActivityDay) => void;
className?: string;
};
export function MostActiveDaysCard({
totalSteps,
selectedDay = null,
onSelectDay,
className,
}: MostActiveDaysCardProps) {
const scrollerRef = useRef<HTMLDivElement>(null);
const monthRefs = useRef<Array<HTMLDivElement | null>>([]);
const [visibleMonth, setVisibleMonth] = useState(0);
const scrollToMonth = (
requestedMonth: number,
behavior: ScrollBehavior = "smooth",
) => {
const month = Math.min(11, Math.max(0, requestedMonth));
const scroller = scrollerRef.current;
const monthElement = monthRefs.current[month];
if (!scroller || !monthElement) return;
const monthOffset =
monthElement.getBoundingClientRect().top -
scroller.getBoundingClientRect().top +
scroller.scrollTop;
scroller.scrollTo({
top: Math.max(0, monthOffset - 9),
behavior,
});
};
useEffect(() => {
const frame = requestAnimationFrame(() => scrollToMonth(6, "smooth"));
return () => cancelAnimationFrame(frame);
}, []);
useEffect(() => {
const scroller = scrollerRef.current;
if (!scroller) return;
let frame = 0;
const updateVisibleMonth = () => {
const scrollerTop = scroller.getBoundingClientRect().top;
let activeMonth = 0;
monthRefs.current.forEach((monthElement, month) => {
if (
monthElement &&
monthElement.getBoundingClientRect().top - scrollerTop <= 24
) {
activeMonth = month;
}
});
setVisibleMonth(activeMonth);
};
const onScroll = () => {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(updateVisibleMonth);
};
scroller.addEventListener("scroll", onScroll, { passive: true });
return () => {
cancelAnimationFrame(frame);
scroller.removeEventListener("scroll", onScroll);
};
}, []);
return (
<section
className={cx(
"flex h-[330px] w-full min-w-0 flex-col gap-4 rounded-[20px] bg-background-secondary-default p-2.5",
className,
)}
>
<div className="flex w-full items-start justify-between gap-2 px-1.5 pt-1.5">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<p className="text-body-medium whitespace-nowrap text-text-secondary">
Most active days
</p>
<div className="flex items-baseline gap-1">
<p className="text-title-1-medium whitespace-nowrap text-text-primary tabular-nums">
{totalSteps.toLocaleString()}
</p>
<span className="text-caption-1-medium whitespace-nowrap text-text-secondary">
total steps
</span>
</div>
</div>
<WeekRangePill
label={ACTIVITY_MONTHS[visibleMonth]}
onPrev={() => scrollToMonth(visibleMonth - 1)}
onNext={() => scrollToMonth(visibleMonth + 1)}
className="w-[128px]"
/>
</div>
<div className="min-h-0 w-full flex-1 overflow-hidden rounded-2lg bg-background-inner-default px-2.5">
<div
ref={scrollerRef}
className="h-full w-full overflow-y-auto overscroll-contain"
>
<div className="relative flex flex-col gap-4 py-2.5">
{ACTIVITY_MONTHS.map((monthName, month) => {
const dayCount = new Date(ACTIVITY_YEAR, month + 1, 0).getDate();
return (
<div
key={monthName}
data-month={month}
ref={(element) => {
monthRefs.current[month] = element;
}}
className="flex flex-col gap-2.5"
>
<p className="pl-1 text-title-3-medium text-text-primary">
{monthName.slice(0, 3)}
</p>
<div className="grid grid-cols-7 gap-x-[5px]">
{Array.from({ length: dayCount }, (_, index) => index + 1).map(
(day) => {
const selected =
selectedDay?.month === month && selectedDay.day === day;
return (
<button
key={day}
type="button"
onClick={() => onSelectDay?.({ month, day })}
aria-pressed={selected}
aria-label={`Activity for ${monthName} ${day}`}
className={cx(
"flex h-[78px] cursor-pointer flex-col items-center justify-center gap-2.5 rounded-2lg border-2 p-2.5",
"outline-none transition-colors duration-150 ease focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-border-focus-ring",
selected
? "border-border-button-hover bg-background-primary-default"
: "border-transparent hover:bg-background-primary-hover",
)}
>
<span className="text-body-medium whitespace-nowrap text-text-primary">
{day}
</span>
<MiniActivityRings month={month} day={day} />
</button>
);
},
)}
</div>
</div>
);
})}
</div>
</div>
</div>
</section>
);
}"use client";
import { useEffect, useRef, useState } from "react";
import { WeekRangePill } from "@/components/blocks/medical/week-range-pill";
import {
ACTIVITY_MONTHS,
ACTIVITY_YEAR,
activityRingPct,
} from "@/components/charts/activity-rings-card";
import type { ActivityDay } from "@/components/charts/activity-rings-card";
import { cx } from "@/utils/cx";
const MINI_RINGS = [
{ radius: 12, color: "var(--color-chart-3-active)" },
{ radius: 8.4, color: "var(--color-chart-2)" },
{ radius: 4.8, color: "var(--color-chart-4)" },
] as const;
function MiniActivityRings({ month, day }: ActivityDay) {
// The BoardCN preview represents dates after its July 10 checkpoint as
// future days. Their tracks remain visible, but progress is intentionally
// omitted.
const isFutureDay = month > 6 || (month === 6 && day > 10);
return (
<svg viewBox="0 0 28 28" className="size-7 shrink-0" aria-hidden="true">
{MINI_RINGS.map((ring, index) => {
const circumference = 2 * Math.PI * ring.radius;
const progress = circumference * activityRingPct(month, day, index);
return (
<g key={ring.radius} transform="rotate(-90 14 14)">
<circle
cx={14}
cy={14}
r={ring.radius}
fill="none"
stroke={ring.color}
strokeWidth={2.2}
opacity={0.16}
/>
{!isFutureDay && (
<circle
cx={14}
cy={14}
r={ring.radius}
fill="none"
stroke={ring.color}
strokeWidth={2.2}
strokeLinecap="round"
strokeDasharray={`${progress} ${circumference - progress}`}
/>
)}
</g>
);
})}
</svg>
);
}
export type MostActiveDaysCardProps = {
/** Headline total shown above the calendar. */
totalSteps: number;
/** Currently selected calendar date. */
selectedDay?: ActivityDay | null;
/** Called when a day is selected. Pair with ActivityRingsCard to update it. */
onSelectDay?: (day: ActivityDay) => void;
className?: string;
};
export function MostActiveDaysCard({
totalSteps,
selectedDay = null,
onSelectDay,
className,
}: MostActiveDaysCardProps) {
const scrollerRef = useRef<HTMLDivElement>(null);
const monthRefs = useRef<Array<HTMLDivElement | null>>([]);
const [visibleMonth, setVisibleMonth] = useState(0);
const scrollToMonth = (
requestedMonth: number,
behavior: ScrollBehavior = "smooth",
) => {
const month = Math.min(11, Math.max(0, requestedMonth));
const scroller = scrollerRef.current;
const monthElement = monthRefs.current[month];
if (!scroller || !monthElement) return;
const monthOffset =
monthElement.getBoundingClientRect().top -
scroller.getBoundingClientRect().top +
scroller.scrollTop;
scroller.scrollTo({
top: Math.max(0, monthOffset - 9),
behavior,
});
};
useEffect(() => {
const frame = requestAnimationFrame(() => scrollToMonth(6, "smooth"));
return () => cancelAnimationFrame(frame);
}, []);
useEffect(() => {
const scroller = scrollerRef.current;
if (!scroller) return;
let frame = 0;
const updateVisibleMonth = () => {
const scrollerTop = scroller.getBoundingClientRect().top;
let activeMonth = 0;
monthRefs.current.forEach((monthElement, month) => {
if (
monthElement &&
monthElement.getBoundingClientRect().top - scrollerTop <= 24
) {
activeMonth = month;
}
});
setVisibleMonth(activeMonth);
};
const onScroll = () => {
cancelAnimationFrame(frame);
frame = requestAnimationFrame(updateVisibleMonth);
};
scroller.addEventListener("scroll", onScroll, { passive: true });
return () => {
cancelAnimationFrame(frame);
scroller.removeEventListener("scroll", onScroll);
};
}, []);
return (
<section
className={cx(
"flex h-[330px] w-full min-w-0 flex-col gap-4 rounded-[20px] bg-background-secondary-default p-2.5",
className,
)}
>
<div className="flex w-full items-start justify-between gap-2 px-1.5 pt-1.5">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<p className="text-body-medium whitespace-nowrap text-text-secondary">
Most active days
</p>
<div className="flex items-baseline gap-1">
<p className="text-title-1-medium whitespace-nowrap text-text-primary tabular-nums">
{totalSteps.toLocaleString()}
</p>
<span className="text-caption-1-medium whitespace-nowrap text-text-secondary">
total steps
</span>
</div>
</div>
<WeekRangePill
label={ACTIVITY_MONTHS[visibleMonth]}
onPrev={() => scrollToMonth(visibleMonth - 1)}
onNext={() => scrollToMonth(visibleMonth + 1)}
className="w-[128px]"
/>
</div>
<div className="min-h-0 w-full flex-1 overflow-hidden rounded-2lg bg-background-inner-default px-2.5">
<div
ref={scrollerRef}
className="h-full w-full overflow-y-auto overscroll-contain"
>
<div className="relative flex flex-col gap-4 py-2.5">
{ACTIVITY_MONTHS.map((monthName, month) => {
const dayCount = new Date(ACTIVITY_YEAR, month + 1, 0).getDate();
return (
<div
key={monthName}
data-month={month}
ref={(element) => {
monthRefs.current[month] = element;
}}
className="flex flex-col gap-2.5"
>
<p className="pl-1 text-title-3-medium text-text-primary">
{monthName.slice(0, 3)}
</p>
<div className="grid grid-cols-7 gap-x-[5px]">
{Array.from({ length: dayCount }, (_, index) => index + 1).map(
(day) => {
const selected =
selectedDay?.month === month && selectedDay.day === day;
return (
<button
key={day}
type="button"
onClick={() => onSelectDay?.({ month, day })}
aria-pressed={selected}
aria-label={`Activity for ${monthName} ${day}`}
className={cx(
"flex h-[78px] cursor-pointer flex-col items-center justify-center gap-2.5 rounded-2lg border-2 p-2.5",
"outline-none transition-colors duration-150 ease focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-border-focus-ring",
selected
? "border-border-button-hover bg-background-primary-default"
: "border-transparent hover:bg-background-primary-hover",
)}
>
<span className="text-body-medium whitespace-nowrap text-text-primary">
{day}
</span>
<MiniActivityRings month={month} day={day} />
</button>
);
},
)}
</div>
</div>
);
})}
</div>
</div>
</div>
</section>
);
}Props
Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.
MostActiveDaysCard
| Prop | Type | Default | Description |
|---|---|---|---|
| totalStepsrequired | number | — | Headline total shown above the calendar. |
| className | string | — | — |
| onSelectDay | (day: ActivityDay) => void | — | Called when a day is selected. Pair with ActivityRingsCard to update it. |
| selectedDay | ActivityDay | null | Currently selected calendar date. |