Important Alerts Card
Scrollable alert feed with tinted icon circles and date pills.
Feed
A scrollable alert feed with date pills.
Important alerts
12
this weekHigh Heart rate
Your heart rate rose above 120 BPM while you seemed to be inactive for 10 minutes starting at 8:59 AM, 12 June.
Medical ID
Your emergency contact and allergy information was updated in your Medical ID.
Lab results ready
Your latest blood panel ordered by Dr. Mattheus Clarkson is back — cholesterol and glucose are within the normal range.
Medication reminder
You missed your 9:00 AM dose of Metoprolol. Take it as soon as possible unless your next dose is near.
Irregular sleep
Your bedtime shifted by more than 2 hours on 3 of the last 7 nights, which can affect your sleep score.
Low blood oxygen
Your blood oxygen dipped to 93% for a short period during sleep on the night of 4 June.
function ImportantAlertsDemo() {
return (
<ImportantAlertsCard
alerts={IMPORTANT_ALERTS}
weekCount={IMPORTANT_ALERTS_WEEK_COUNT}
weekRangeLabel={IMPORTANT_ALERTS_WEEK_RANGE}
/>
);
}function ImportantAlertsDemo() {
return (
<ImportantAlertsCard
alerts={IMPORTANT_ALERTS}
weekCount={IMPORTANT_ALERTS_WEEK_COUNT}
weekRangeLabel={IMPORTANT_ALERTS_WEEK_RANGE}
/>
);
}Installation
npx shadcn@latest add https://boardcn.dev/r/important-alerts-card.jsonnpx shadcn@latest add https://boardcn.dev/r/important-alerts-card.jsonnpm packages
- @internationalized/date
- @remixicon/react
- react-aria-components
BoardCN dependencies
The CLI installs these for you — you do not need to add them yourself.
Source
The 2 files the CLI copies into your project.
"use client";
import { useState } from "react";
import type { ComponentType } from "react";
import { WeekRangePill } from "@/components/blocks/medical/week-range-pill";
import { cx } from "@/utils/cx";
type IconComponent = ComponentType<{ className?: string; "aria-hidden"?: boolean | "true" | "false" }>;
export type ImportantAlert = {
icon: IconComponent;
iconBg: string;
title: string;
description: string;
date: string;
};
export type ImportantAlertsCardProps = {
alerts: readonly ImportantAlert[];
/** Total alerts logged this week — may exceed `alerts.length` when the feed is a recent subset. */
weekCount: number;
weekRangeLabel: string;
className?: string;
};
export function ImportantAlertsCard({
alerts,
weekCount,
weekRangeLabel,
className,
}: ImportantAlertsCardProps) {
const [scrolled, setScrolled] = useState(false);
return (
<section
className={cx(
// The 1px border matches the card's own bg — invisible as a line,
// but content clips at the padding box (inside the border), so the
// white alert rows stop 1px shy of the rounded edge instead of
// touching it when scrolled to the bottom.
"flex h-[330px] w-full min-w-0 flex-col gap-4 overflow-hidden rounded-[20px] border border-background-secondary-default bg-background-secondary-default px-2.5 pt-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 text-text-secondary">Important alerts</p>
<div className="flex items-baseline gap-1">
<p className="text-title-1-medium whitespace-nowrap text-text-primary tabular-nums">
{weekCount}
</p>
<span className="text-caption-1-medium whitespace-nowrap text-text-secondary">this week</span>
</div>
</div>
<WeekRangePill label={weekRangeLabel} />
</div>
<div className="relative min-h-0 w-full flex-1">
<div
className="flex h-full w-full flex-col gap-2.5 overflow-y-auto overscroll-contain pb-2.5"
onScroll={(e) => setScrolled(e.currentTarget.scrollTop > 0)}
>
{alerts.map((alert, index) => (
<div
key={`${alert.title}-${index}`}
className="relative flex w-full shrink-0 flex-col gap-2 rounded-2lg bg-background-inner-default p-2.5"
>
<span className={cx("flex size-8 shrink-0 items-center justify-center rounded-full", alert.iconBg)}>
<alert.icon className="size-[18px] text-white" aria-hidden />
</span>
<div className="flex flex-col gap-0.5">
<p className="text-body-medium whitespace-nowrap text-text-primary">{alert.title}</p>
<p className="text-body-2-regular text-text-secondary">{alert.description}</p>
</div>
<span className="absolute top-2.5 right-2.5 rounded-md bg-background-secondary-default px-1.5 py-px text-body-medium whitespace-nowrap text-text-secondary">
{alert.date}
</span>
</div>
))}
</div>
{/* Top fade — card-colored gradient that eases in once the feed is
scrolled, so rows dissolve under the header instead of cutting. */}
<div
aria-hidden
className={cx(
"pointer-events-none absolute inset-x-0 top-0 h-10 bg-linear-to-b from-background-secondary-default to-transparent",
"transition-opacity duration-200 ease-out",
scrolled ? "opacity-100" : "opacity-0",
)}
/>
</div>
</section>
);
}"use client";
import { useState } from "react";
import type { ComponentType } from "react";
import { WeekRangePill } from "@/components/blocks/medical/week-range-pill";
import { cx } from "@/utils/cx";
type IconComponent = ComponentType<{ className?: string; "aria-hidden"?: boolean | "true" | "false" }>;
export type ImportantAlert = {
icon: IconComponent;
iconBg: string;
title: string;
description: string;
date: string;
};
export type ImportantAlertsCardProps = {
alerts: readonly ImportantAlert[];
/** Total alerts logged this week — may exceed `alerts.length` when the feed is a recent subset. */
weekCount: number;
weekRangeLabel: string;
className?: string;
};
export function ImportantAlertsCard({
alerts,
weekCount,
weekRangeLabel,
className,
}: ImportantAlertsCardProps) {
const [scrolled, setScrolled] = useState(false);
return (
<section
className={cx(
// The 1px border matches the card's own bg — invisible as a line,
// but content clips at the padding box (inside the border), so the
// white alert rows stop 1px shy of the rounded edge instead of
// touching it when scrolled to the bottom.
"flex h-[330px] w-full min-w-0 flex-col gap-4 overflow-hidden rounded-[20px] border border-background-secondary-default bg-background-secondary-default px-2.5 pt-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 text-text-secondary">Important alerts</p>
<div className="flex items-baseline gap-1">
<p className="text-title-1-medium whitespace-nowrap text-text-primary tabular-nums">
{weekCount}
</p>
<span className="text-caption-1-medium whitespace-nowrap text-text-secondary">this week</span>
</div>
</div>
<WeekRangePill label={weekRangeLabel} />
</div>
<div className="relative min-h-0 w-full flex-1">
<div
className="flex h-full w-full flex-col gap-2.5 overflow-y-auto overscroll-contain pb-2.5"
onScroll={(e) => setScrolled(e.currentTarget.scrollTop > 0)}
>
{alerts.map((alert, index) => (
<div
key={`${alert.title}-${index}`}
className="relative flex w-full shrink-0 flex-col gap-2 rounded-2lg bg-background-inner-default p-2.5"
>
<span className={cx("flex size-8 shrink-0 items-center justify-center rounded-full", alert.iconBg)}>
<alert.icon className="size-[18px] text-white" aria-hidden />
</span>
<div className="flex flex-col gap-0.5">
<p className="text-body-medium whitespace-nowrap text-text-primary">{alert.title}</p>
<p className="text-body-2-regular text-text-secondary">{alert.description}</p>
</div>
<span className="absolute top-2.5 right-2.5 rounded-md bg-background-secondary-default px-1.5 py-px text-body-medium whitespace-nowrap text-text-secondary">
{alert.date}
</span>
</div>
))}
</div>
{/* Top fade — card-colored gradient that eases in once the feed is
scrolled, so rows dissolve under the header instead of cutting. */}
<div
aria-hidden
className={cx(
"pointer-events-none absolute inset-x-0 top-0 h-10 bg-linear-to-b from-background-secondary-default to-transparent",
"transition-opacity duration-200 ease-out",
scrolled ? "opacity-100" : "opacity-0",
)}
/>
</div>
</section>
);
}"use client";
import { useEffect, useState } from "react";
import { ChevronLeft16, ChevronRight16 } from "@/components/base/date-picker/shared";
import { cx } from "@/utils/cx";
/** Rectangular chevron hover, matched 1:1 to the date picker's month-nav
* buttons (see `MonthPanel` in date-picker/shared). */
const NAV_BUTTON = cx(
"flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[3px] text-text-secondary outline-none",
"transition-colors duration-150 ease hover:bg-background-secondary-hover",
"focus-visible:ring-2 focus-visible:ring-border-focus-ring",
);
/**
* Rolling-counter label: on change the outgoing value lifts 5px + fades out
* while the incoming value rises in from 5px below + fades in. Both labels
* coexist for the ~220ms transition, clipped to the line box so the movement
* reads like a counter tick rather than a crossfade.
*/
function AnimatedLabel({ label }: { label: string }) {
const [current, setCurrent] = useState(label);
const [previous, setPrevious] = useState<string | null>(null);
useEffect(() => {
if (label !== current) {
setPrevious(current);
setCurrent(label);
}
}, [label, current]);
useEffect(() => {
if (previous === null) return;
const t = setTimeout(() => setPrevious(null), 240);
return () => clearTimeout(t);
}, [previous]);
return (
<span className="relative flex-1 overflow-hidden text-center text-body-medium whitespace-nowrap text-text-primary">
{/* Invisible spacer keeps the line box height/width for the absolutes. */}
<span className="invisible">{current}</span>
{previous !== null && (
<span
key={`out-${previous}`}
className="animate-label-out absolute inset-0 flex items-center justify-center"
>
{previous}
</span>
)}
<span
key={`in-${current}`}
className={cx(
"absolute inset-0 flex items-center justify-center",
previous !== null && "animate-label-in",
)}
>
{current}
</span>
</span>
);
}
export function WeekRangePill({
label,
onPrev,
onNext,
className,
}: {
label: string;
onPrev?: () => void;
onNext?: () => void;
/** Override the default 151px width (e.g. the narrower month switcher). */
className?: string;
}) {
const interactive = !!(onPrev || onNext);
return (
<div
className={cx(
"flex h-8 w-[151px] shrink-0 items-center justify-between gap-1 rounded-2lg border border-border-button-default bg-background-primary-default px-1 py-1 shadow-xs",
className,
)}
>
{interactive ? (
<button type="button" aria-label="Previous" onClick={onPrev} className={NAV_BUTTON}>
<ChevronLeft16 />
</button>
) : (
<span className="flex size-4 shrink-0 items-center justify-center text-text-secondary" aria-hidden>
<ChevronLeft16 />
</span>
)}
{/* The interactive switcher rolls its label like a counter as the month
changes; static pills just render plain text. */}
{interactive ? (
<AnimatedLabel label={label} />
) : (
<span className="flex-1 text-center text-body-medium whitespace-nowrap text-text-primary">
{label}
</span>
)}
{interactive ? (
<button type="button" aria-label="Next" onClick={onNext} className={NAV_BUTTON}>
<ChevronRight16 />
</button>
) : (
<span className="flex size-4 shrink-0 items-center justify-center text-text-secondary" aria-hidden>
<ChevronRight16 />
</span>
)}
</div>
);
}"use client";
import { useEffect, useState } from "react";
import { ChevronLeft16, ChevronRight16 } from "@/components/base/date-picker/shared";
import { cx } from "@/utils/cx";
/** Rectangular chevron hover, matched 1:1 to the date picker's month-nav
* buttons (see `MonthPanel` in date-picker/shared). */
const NAV_BUTTON = cx(
"flex size-4 shrink-0 cursor-pointer items-center justify-center rounded-[3px] text-text-secondary outline-none",
"transition-colors duration-150 ease hover:bg-background-secondary-hover",
"focus-visible:ring-2 focus-visible:ring-border-focus-ring",
);
/**
* Rolling-counter label: on change the outgoing value lifts 5px + fades out
* while the incoming value rises in from 5px below + fades in. Both labels
* coexist for the ~220ms transition, clipped to the line box so the movement
* reads like a counter tick rather than a crossfade.
*/
function AnimatedLabel({ label }: { label: string }) {
const [current, setCurrent] = useState(label);
const [previous, setPrevious] = useState<string | null>(null);
useEffect(() => {
if (label !== current) {
setPrevious(current);
setCurrent(label);
}
}, [label, current]);
useEffect(() => {
if (previous === null) return;
const t = setTimeout(() => setPrevious(null), 240);
return () => clearTimeout(t);
}, [previous]);
return (
<span className="relative flex-1 overflow-hidden text-center text-body-medium whitespace-nowrap text-text-primary">
{/* Invisible spacer keeps the line box height/width for the absolutes. */}
<span className="invisible">{current}</span>
{previous !== null && (
<span
key={`out-${previous}`}
className="animate-label-out absolute inset-0 flex items-center justify-center"
>
{previous}
</span>
)}
<span
key={`in-${current}`}
className={cx(
"absolute inset-0 flex items-center justify-center",
previous !== null && "animate-label-in",
)}
>
{current}
</span>
</span>
);
}
export function WeekRangePill({
label,
onPrev,
onNext,
className,
}: {
label: string;
onPrev?: () => void;
onNext?: () => void;
/** Override the default 151px width (e.g. the narrower month switcher). */
className?: string;
}) {
const interactive = !!(onPrev || onNext);
return (
<div
className={cx(
"flex h-8 w-[151px] shrink-0 items-center justify-between gap-1 rounded-2lg border border-border-button-default bg-background-primary-default px-1 py-1 shadow-xs",
className,
)}
>
{interactive ? (
<button type="button" aria-label="Previous" onClick={onPrev} className={NAV_BUTTON}>
<ChevronLeft16 />
</button>
) : (
<span className="flex size-4 shrink-0 items-center justify-center text-text-secondary" aria-hidden>
<ChevronLeft16 />
</span>
)}
{/* The interactive switcher rolls its label like a counter as the month
changes; static pills just render plain text. */}
{interactive ? (
<AnimatedLabel label={label} />
) : (
<span className="flex-1 text-center text-body-medium whitespace-nowrap text-text-primary">
{label}
</span>
)}
{interactive ? (
<button type="button" aria-label="Next" onClick={onNext} className={NAV_BUTTON}>
<ChevronRight16 />
</button>
) : (
<span className="flex size-4 shrink-0 items-center justify-center text-text-secondary" aria-hidden>
<ChevronRight16 />
</span>
)}
</div>
);
}Props
Generated from the component's TypeScript types. Standard DOM and React Aria props are omitted.
ImportantAlertsCard
| Prop | Type | Default | Description |
|---|---|---|---|
| alertsrequired | readonly ImportantAlert[] | — | — |
| weekCountrequired | number | — | Total alerts logged this week — may exceed `alerts.length` when the feed is a recent subset. |
| weekRangeLabelrequired | string | — | — |
| className | string | — | — |
WeekRangePill
| Prop | Type | Default | Description |
|---|---|---|---|
| labelrequired | string | — | — |
| className | string | — | Override the default 151px width (e.g. the narrower month switcher). |
| onNext | () => void | — | — |
| onPrev | () => void | — | — |