For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Alerts Card 01

A compact, scrollable feed of recent alerts.

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>
  );
}