- Accordion
- Agent Thinking
- Alert
- Alert Dialog
- Announcement
- Aspect Ratio
- Attachment
- Avatar
- Badge
- Breadcrumb
- Bubble
- Button
- Button Group
- Calendar
- Card
- Carousel
- Checkbox
- Checkbox Card
- Code Block
- Collapsible
- Color Picker
- Combobox
- Command
- Composer
- Composer Loader
- Context Menu
- Data Grid
- Data Table
- Date Picker
- Dialog
- Direction
- Drawer
- Dropdown Menu
- Empty
- Field
- File Upload
- Hover Card
- Input
- Input Group
- Input Otp
- Item
- Kbd
- Label
- Marker
- Menubar
- Message
- Message Scroller
- Meter
- Native Select
- Navigation Menu
- Number Field
- Pagination
- Period Switcher
- Popover
- Progress
- Questionnaire
- Radio Card
- Radio Group
- Resizable
- Reveal
- Scroll Area
- Search Field
- Select
- Separator
- Sheet
- Sidebar
- Skeleton
- Slider
- Social Button
- Sonner
- Spinner
- Status Dot
- Switch
- Switch Card
- Table
- Tabs
- Tag Group
- Textarea
- Theme Toggle
- Time Field
- Toggle
- Toggle Group
- Tooltip
- Trace
- Tree View
- Agent Creator 01
- Agent Limits Card 01
- Agent Progress 01
- AI Chat 01
- AI Image Generation 01
- AI Profile 01
- Alerts Card 01
- Auth Card 01
- Calendar 01
- Composer 01
- Composer Attachments 01
- Composer Panel 01
- Finance Dashboard 01
- Home Dashboard 01
- Marketing Dashboard 01
- Medical Profile 01
- Meeting Scheduler 01
- Multi Agent Chat 01
- Notification Center 01
- Patient Info Card 01
- Project Board 01
- Questionnaire 01
- Settings Modal 01
- Sidebar 01
- Stat Cards 01
- Task List 01
- Web Search 01
- Activity Rings Card 01
- Area Chart Card 01
- Bar List Card 01
- Combo Chart Card 01
- Contributions Card 01
- Earnings Chart Card 01
- Funnel Chart Card 01
- Heatmap Chart Card 01
- Line Chart Card 01
- Most Active Days Card 01
- Orders Chart Card 01
- Radar Chart Card 01
- Radial Chart Card 01
- Revenue Chart Card 01
- Sankey Chart Card 01
- Scatter Chart Card 01
- Sleep Score Card 01
- Stage Bars Card 01
- Steps Card 01
Recent alerts
0
this week29 Jun - 5 Jul
Activity check
Your activity was higher than usual during your afternoon walk.
Profile updated
Your emergency contact information has been saved.
Results available
Your latest report is ready to review in your account.
Reminder
You have a scheduled reminder for this evening.
Sleep update
Your sleep schedule changed over the past week.
Daily summary
Your daily summary is ready to view.
Installation
$ pnpm dlx shadcn@latest add @cobalt/alerts-card-01
Use the arrows to navigate between weeks. Replace the sample alerts and dates with your own data and connect the selected week to your alert feed.
Source
"use client";
import {
RiAsterisk,
RiCapsuleLine,
RiFlaskLine,
RiMoonLine,
RiPulseLine,
RiWindyLine,
} from "@remixicon/react";
import { cn } from "cn";
import { useEffect, useRef, useState } from "react";
import { useReducedMotion } from "@/components/hooks/use-reduced-motion";
import { Badge } from "@/components/ui/badge";
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
PeriodSwitcher,
PeriodSwitcherLabel,
PeriodSwitcherNext,
PeriodSwitcherPrevious,
} from "@/components/ui/period-switcher";
import { Reveal } from "@/components/ui/reveal";
const alerts = [
{
icon: RiPulseLine,
color:
"bg-[var(--alerts-icon-rose,var(--destructive))] text-[var(--alerts-icon-light,var(--primary-foreground))]",
title: "Activity check",
description:
"Your activity was higher than usual during your afternoon walk.",
date: "Jul 5",
},
{
icon: RiAsterisk,
color:
"bg-[var(--alerts-icon-amber,var(--secondary))] text-[var(--alerts-icon-light,var(--primary-foreground))] dark:text-[var(--alerts-icon-dark,var(--secondary-foreground))]",
title: "Profile updated",
description: "Your emergency contact information has been saved.",
date: "Jul 4",
},
{
icon: RiFlaskLine,
color:
"bg-[var(--alerts-icon-green,var(--secondary))] text-[var(--alerts-icon-dark,var(--secondary-foreground))]",
title: "Results available",
description: "Your latest report is ready to review in your account.",
date: "Jul 3",
},
{
icon: RiCapsuleLine,
color:
"bg-[var(--alerts-icon-blue,var(--secondary))] text-[var(--alerts-icon-dark,var(--secondary-foreground))]",
title: "Reminder",
description: "You have a scheduled reminder for this evening.",
date: "Jul 2",
},
{
icon: RiMoonLine,
color:
"bg-chart-5 text-[var(--alerts-icon-dark,var(--secondary-foreground))]",
title: "Sleep update",
description: "Your sleep schedule changed over the past week.",
date: "Jul 1",
},
{
icon: RiWindyLine,
color:
"bg-chart-1 text-[var(--alerts-icon-dark,var(--secondary-foreground))]",
title: "Daily summary",
description: "Your daily summary is ready to view.",
date: "Jun 30",
},
];
/** Alerts logged this week — higher than `alerts.length` because the feed
* only lists the most recent ones, not the full history. */
const alertsThisWeek = 12;
const monthFormatter = new Intl.DateTimeFormat("en-US", {
month: "short",
timeZone: "UTC",
});
// "29 Jun - 5 Jul", or "22 - 28 Jun" when the week stays in one month.
function weekLabel(offset: number) {
const start = new Date(Date.UTC(2026, 5, 29 + offset * 7));
const end = new Date(Date.UTC(2026, 5, 29 + offset * 7 + 6));
const endText = `${end.getUTCDate()} ${monthFormatter.format(end)}`;
return start.getUTCMonth() === end.getUTCMonth()
? `${start.getUTCDate()} - ${endText}`
: `${start.getUTCDate()} ${monthFormatter.format(start)} - ${endText}`;
}
// Same count-up as the orders-chart-card-01 block: ease-out
// over 320ms, and the final value straight away when motion is reduced.
function useCountUp(target: number, reduced: boolean) {
const [display, setDisplay] = useState(0);
const fromRef = useRef(0);
useEffect(() => {
const from = fromRef.current;
if (reduced) {
fromRef.current = target;
setDisplay(target);
return;
}
if (from === target) {
return;
}
const start = performance.now();
let frame = 0;
const tick = (now: number) => {
const t = Math.min(1, (now - start) / 320);
const value = Math.round(from + (target - from) * (1 - (1 - t) ** 3));
fromRef.current = value;
setDisplay(value);
if (t < 1) {
frame = requestAnimationFrame(tick);
} else {
fromRef.current = target;
}
};
frame = requestAnimationFrame(tick);
return () => cancelAnimationFrame(frame);
}, [target, reduced]);
return reduced ? target : display;
}
export function AlertsCard({ className }: { className?: string }) {
const [scrolled, setScrolled] = useState(false);
const [week, setWeek] = useState(0);
const reduced = useReducedMotion();
const count = useCountUp(alertsThisWeek, reduced);
return (
<Card
radius={20}
variant="muted"
size="xs"
role="region"
aria-label="Recent alerts"
className={cn("@container h-82.5 w-full min-w-0", className)}
>
<CardHeader className="flex items-start justify-between">
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
<CardTitle variant="secondary" className="truncate">
Recent alerts
</CardTitle>
<div className="flex items-baseline gap-1">
<p className="text-2xl leading-8.5 font-medium whitespace-nowrap tabular-nums">
{count}
</p>
{/* Secondary label: dropped in a narrow card instead of squeezing
"Recent alerts" down to a single letter. */}
<span className="hidden text-xs leading-4 font-medium whitespace-nowrap text-muted-foreground @min-[240px]/card-header:inline">
this week
</span>
</div>
</div>
<PeriodSwitcher>
<PeriodSwitcherPrevious
tooltip="Previous week"
onClick={() => setWeek((current) => current - 1)}
/>
<PeriodSwitcherLabel className="min-w-[110px]">
{weekLabel(week)}
</PeriodSwitcherLabel>
<PeriodSwitcherNext
tooltip="Next week"
onClick={() => setWeek((current) => current + 1)}
/>
</PeriodSwitcher>
</CardHeader>
<CardContent className="min-h-0 flex-1">
<div className="relative min-h-0 w-full flex-1">
<div
role="region"
// Scrollable region: focusable so keyboard users can scroll it.
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
aria-label="Alert feed"
className="flex h-full w-full flex-col gap-2.5 overflow-y-auto overscroll-contain focus-visible:outline-2 focus-visible:outline-ring"
onScroll={(event) => setScrolled(event.currentTarget.scrollTop > 0)}
>
{alerts.map(({ icon: Icon, ...item }, index) => (
<Reveal key={item.title} index={index} className="shrink-0">
<Card
radius={10}
variant="raised"
size="xs"
className="relative w-full"
>
<CardContent className="gap-2">
<span
className={cn(
"flex size-8 shrink-0 items-center justify-center rounded-full",
item.color
)}
>
<Icon className="size-4.5" aria-hidden />
</span>
<div className="flex min-w-0 flex-col gap-0.5">
<p className="min-w-0 pe-12 text-sm leading-5 font-medium wrap-break-word text-foreground">
{item.title}
</p>
{/* Secondary copy, hidden before it can be squeezed into an
unreadable column. The row grows with the text. */}
{/* oxlint-disable-next-line shadcn/no-arbitrary-values -- reference: alert description is 13px (measured: "Your" 54px vs 59px at 14px, 2x) in the secondary grey */}
<p className="hidden text-[13px] leading-4.5 text-(--foreground-secondary,var(--muted-foreground)) @min-[260px]:block">
{item.description}
</p>
</div>
</CardContent>
<Badge
variant="label"
tone="soft"
size="xs"
className="absolute end-2.5 top-2.5"
>
{item.date}
</Badge>
</Card>
</Reveal>
))}
</div>
<div
aria-hidden
className={cn(
"pointer-events-none absolute inset-x-0 top-0 h-10 bg-linear-to-b from-surface-sunken to-transparent transition-opacity duration-200 ease-out motion-reduce:transition-none",
scrolled ? "opacity-100" : "opacity-0"
)}
/>
</div>
</CardContent>
</Card>
);
} On this page