Alert
Free, copy-and-go Alert components built on the SevenUI Alert primitive.Read the primitive docs.
Scheduled maintenance on Sunday
The dashboard will be read-only from 02:00 to 03:30 UTC.
Domain verified
acme.com is now sending mail with DKIM and SPF records.
You have used 92% of your storage
Uploads will pause at 100 GB. Archive old projects or upgrade.
Payment failed
Your card ending in 4242 was declined. Update it to keep your plan.
"use client";
import {
CircleAlertIcon,
CircleCheckIcon,
InfoIcon,
TriangleAlertIcon,
} from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
export default function Alert01() {
return (
<div className="grid w-full max-w-md gap-3">
<Alert role="status" className="border-chart-2/30 bg-chart-2/5">
<InfoIcon aria-hidden="true" className="text-chart-2!" />
<AlertTitle>Scheduled maintenance on Sunday</AlertTitle>
<AlertDescription>
The dashboard will be read-only from 02:00 to 03:30 UTC.
</AlertDescription>
</Alert>
<Alert role="status" className="border-success/30 bg-success/5">
<CircleCheckIcon aria-hidden="true" className="text-success!" />
<AlertTitle>Domain verified</AlertTitle>
<AlertDescription>
acme.com is now sending mail with DKIM and SPF records.
</AlertDescription>
</Alert>
<Alert className="border-warning/40 bg-warning/5">
<TriangleAlertIcon aria-hidden="true" className="text-warning!" />
<AlertTitle>You have used 92% of your storage</AlertTitle>
<AlertDescription>
Uploads will pause at 100 GB. Archive old projects or upgrade.
</AlertDescription>
</Alert>
<Alert
variant="destructive"
className="border-destructive/30 bg-destructive/5"
>
<CircleAlertIcon aria-hidden="true" />
<AlertTitle>Payment failed</AlertTitle>
<AlertDescription>
Your card ending in 4242 was declined. Update it to keep your plan.
</AlertDescription>
</Alert>
</div>
);
}
npx shadcn@latest add @sevenui/component/alert-01pnpm dlx shadcn@latest add @sevenui/component/alert-01yarn dlx shadcn@latest add @sevenui/component/alert-01bunx --bun shadcn@latest add @sevenui/component/alert-01Compact
Sync paused on this device
Changes are saved locally and upload once you are back online.
Default
Sync paused on this device
Changes are saved locally and upload once you are back online.
Comfortable
Sync paused on this device
Changes are saved locally and upload once you are back online.
"use client";
import { CloudIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
const densities = [
{
label: "Compact",
className: "gap-0 px-2 py-1.5 text-xs",
iconClassName: "size-3.5",
titleClassName: "text-xs",
descriptionClassName: "text-xs",
},
{
label: "Default",
className: "",
iconClassName: "size-4",
titleClassName: "",
descriptionClassName: "",
},
{
label: "Comfortable",
className: "gap-1 px-4 py-3.5 has-[>svg]:gap-x-3",
iconClassName: "size-5",
titleClassName: "text-base",
descriptionClassName: "",
},
];
export default function Alert02() {
return (
<div className="grid w-full max-w-md gap-5">
{densities.map((density) => (
<div key={density.label} className="grid gap-1.5">
<span className="text-xs font-medium text-muted-foreground">
{density.label}
</span>
<Alert role="status" className={density.className}>
<CloudIcon aria-hidden="true" className={density.iconClassName} />
<AlertTitle className={density.titleClassName}>
Sync paused on this device
</AlertTitle>
<AlertDescription className={density.descriptionClassName}>
Changes are saved locally and upload once you are back online.
</AlertDescription>
</Alert>
</div>
))}
</div>
);
}
npx shadcn@latest add @sevenui/component/alert-02pnpm dlx shadcn@latest add @sevenui/component/alert-02yarn dlx shadcn@latest add @sevenui/component/alert-02bunx --bun shadcn@latest add @sevenui/component/alert-02Filled
Two-factor authentication is off
Protect your workspace with an authenticator app or security key.
Subtle
Two-factor authentication is off
Protect your workspace with an authenticator app or security key.
Outline
Two-factor authentication is off
Protect your workspace with an authenticator app or security key.
Ghost
Two-factor authentication is off
Protect your workspace with an authenticator app or security key.
"use client";
import { ShieldAlertIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
const emphasis = [
{
label: "Filled",
className: "border-primary bg-primary text-primary-foreground",
descriptionClassName: "text-primary-foreground/80",
},
{
label: "Subtle",
className: "border-transparent bg-muted",
descriptionClassName: "",
},
{
label: "Outline",
className: "bg-transparent",
descriptionClassName: "",
},
{
label: "Ghost",
className: "border-transparent bg-transparent px-0",
descriptionClassName: "",
},
];
export default function Alert03() {
return (
<div className="grid w-full max-w-md gap-5">
{emphasis.map((item) => (
<div key={item.label} className="grid gap-1.5">
<span className="text-xs font-medium text-muted-foreground">
{item.label}
</span>
<Alert className={item.className}>
<ShieldAlertIcon aria-hidden="true" />
<AlertTitle>Two-factor authentication is off</AlertTitle>
<AlertDescription className={item.descriptionClassName}>
Protect your workspace with an authenticator app or security key.
</AlertDescription>
</Alert>
</div>
))}
</div>
);
}
npx shadcn@latest add @sevenui/component/alert-03pnpm dlx shadcn@latest add @sevenui/component/alert-03yarn dlx shadcn@latest add @sevenui/component/alert-03bunx --bun shadcn@latest add @sevenui/component/alert-03You are offline
We could not reach the server. Your draft is safe and will sync when the connection returns.
"use client";
import * as React from "react";
import { CloudOffIcon, WifiOffIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
type Mode = "offline" | "retrying" | "local";
export default function Alert04() {
const [mode, setMode] = React.useState<Mode>("offline");
const [attempts, setAttempts] = React.useState(0);
React.useEffect(() => {
if (mode !== "retrying") return;
// The server stays unreachable so the offline state can be replayed.
const timer = setTimeout(() => setMode("offline"), 1200);
return () => clearTimeout(timer);
}, [mode]);
if (mode === "local") {
return (
<Alert
role="status"
className="w-full max-w-sm grid-cols-[auto_1fr_auto] items-center gap-x-2"
>
<CloudOffIcon aria-hidden="true" className="row-span-1! translate-y-0!" />
<AlertTitle className="col-start-2">Working offline</AlertTitle>
<Button
size="xs"
variant="outline"
className="col-start-3 row-start-1"
onClick={() => setMode("retrying")}
>
Reconnect
</Button>
</Alert>
);
}
const retrying = mode === "retrying";
return (
<Alert className="w-full max-w-sm justify-items-center gap-1 px-6 py-8 text-center shadow-sm">
<div className="mb-3 flex size-11 items-center justify-center rounded-full bg-muted">
<WifiOffIcon aria-hidden="true" className="size-5 text-foreground" />
</div>
<AlertTitle className="text-base">You are offline</AlertTitle>
<AlertDescription className="max-w-64" aria-live="polite">
{retrying
? "Trying to reach the server…"
: attempts > 0
? "Still no connection. Your draft is safe and will sync when the connection returns."
: "We could not reach the server. Your draft is safe and will sync when the connection returns."}
</AlertDescription>
<div className="mt-4 flex w-full flex-col gap-2 sm:flex-row sm:justify-center">
<Button
size="sm"
disabled={retrying}
onClick={() => {
setAttempts((count) => count + 1);
setMode("retrying");
}}
>
{retrying ? <Spinner data-icon="inline-start" aria-hidden="true" /> : null}
{retrying ? "Retrying" : "Try again"}
</Button>
<Button
size="sm"
variant="ghost"
disabled={retrying}
onClick={() => setMode("local")}
>
Work offline
</Button>
</div>
</Alert>
);
}
npx shadcn@latest add @sevenui/component/alert-04pnpm dlx shadcn@latest add @sevenui/component/alert-04yarn dlx shadcn@latest add @sevenui/component/alert-04bunx --bun shadcn@latest add @sevenui/component/alert-04Workflows now run in parallel
Independent steps start at the same time, cutting average run time by 40%. Read the changelog
"use client";
import * as React from "react";
import { MegaphoneIcon, RotateCcwIcon, XIcon } from "lucide-react";
import {
Alert,
AlertAction,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
export default function Alert05() {
const [open, setOpen] = React.useState(true);
return (
<div className="grid w-full max-w-md gap-3">
{open ? (
<Alert role="status">
<MegaphoneIcon aria-hidden="true" />
<AlertTitle>Workflows now run in parallel</AlertTitle>
<AlertDescription>
Independent steps start at the same time, cutting average run time
by 40%. <a href="#changelog">Read the changelog</a>
</AlertDescription>
<AlertAction>
<Button
variant="ghost"
size="icon-xs"
aria-label="Dismiss announcement"
onClick={() => setOpen(false)}
>
<XIcon aria-hidden="true" />
</Button>
</AlertAction>
</Alert>
) : (
<div className="flex items-center justify-between gap-3 rounded-lg border border-dashed px-2.5 py-2 text-sm text-muted-foreground">
<span>Announcement dismissed.</span>
<Button variant="outline" size="xs" onClick={() => setOpen(true)}>
<RotateCcwIcon aria-hidden="true" data-icon="inline-start" />
Show again
</Button>
</div>
)}
</div>
);
}
npx shadcn@latest add @sevenui/component/alert-05pnpm dlx shadcn@latest add @sevenui/component/alert-05yarn dlx shadcn@latest add @sevenui/component/alert-05bunx --bun shadcn@latest add @sevenui/component/alert-05MR
Maya Ruiz requested your review
Pull request 482 “Migrate billing webhooks to queue workers” is waiting on you. Due today.
"use client";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
export default function Alert06() {
return (
<Alert
role="status"
className="w-full max-w-md grid-cols-[auto_1fr] gap-x-3 px-3 py-3"
>
<Avatar className="row-span-3">
<AvatarImage src="/placeholder.svg" alt="" />
<AvatarFallback>MR</AvatarFallback>
</Avatar>
<AlertTitle className="col-start-2">
Maya Ruiz requested your review
</AlertTitle>
<AlertDescription className="col-start-2">
Pull request 482 “Migrate billing webhooks to queue
workers” is waiting on you. Due today.
</AlertDescription>
<div className="col-start-2 mt-2 flex flex-wrap gap-2">
<Button size="xs">Start review</Button>
<Button size="xs" variant="outline">
Reassign
</Button>
</div>
</Alert>
);
}
npx shadcn@latest add @sevenui/component/alert-06pnpm dlx shadcn@latest add @sevenui/component/alert-06yarn dlx shadcn@latest add @sevenui/component/alert-06bunx --bun shadcn@latest add @sevenui/component/alert-06"use client";
import * as React from "react";
import { CircleAlertIcon, CircleCheckIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const fields = [
{
id: "alert-07-company",
name: "company",
label: "Company name",
autoComplete: "organization",
validate: (value: string) =>
value.trim().length < 2 ? "Enter your company name" : null,
},
{
id: "alert-07-vat",
name: "vat",
label: "VAT number",
autoComplete: "off",
validate: (value: string) =>
/^[A-Z]{2}[0-9A-Z]{8,12}$/.test(value.trim().toUpperCase())
? null
: "Enter a VAT number that starts with a country code, like DE123456789",
},
{
id: "alert-07-email",
name: "email",
label: "Billing email",
autoComplete: "email",
validate: (value: string) =>
/^\S+@\S+\.\S+$/.test(value.trim())
? null
: "Enter an email address like finance@company.com",
},
];
type Values = Record<string, string>;
function validateAll(values: Values) {
return fields.flatMap((field) => {
const message = field.validate(values[field.name] ?? "");
return message ? [{ id: field.id, message }] : [];
});
}
const initialValues: Values = {
company: "Brightline Studio",
vat: "123456",
email: "finance@brightline",
};
export default function Alert07() {
const summaryRef = React.useRef<HTMLDivElement>(null);
const [values, setValues] = React.useState<Values>(initialValues);
const [errors, setErrors] = React.useState<{ id: string; message: string }[]>(
() => validateAll(initialValues),
);
const [saved, setSaved] = React.useState(false);
function submit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const next = validateAll(values);
setErrors(next);
setSaved(next.length === 0);
if (next.length > 0) {
requestAnimationFrame(() => summaryRef.current?.focus());
}
}
function errorFor(id: string) {
return errors.find((error) => error.id === id)?.message;
}
return (
<form noValidate onSubmit={submit} className="grid w-full max-w-sm gap-4">
<div className="grid gap-1">
<h3 className="font-medium">Billing details</h3>
<p className="text-sm text-muted-foreground">
These appear on every invoice we send you.
</p>
</div>
{errors.length > 0 && (
<Alert
ref={summaryRef}
tabIndex={-1}
variant="destructive"
className="border-destructive/30 outline-none focus-visible:ring-3 focus-visible:ring-destructive/20"
>
<CircleAlertIcon aria-hidden="true" />
<AlertTitle>
Fix {errors.length} {errors.length === 1 ? "field" : "fields"} to
save
</AlertTitle>
<AlertDescription>
<ul className="mt-1 grid gap-1">
{errors.map((error) => (
<li key={error.id}>
<a
href={`#${error.id}`}
className="rounded-sm text-destructive outline-none focus-visible:ring-3 focus-visible:ring-destructive/30"
onClick={(event) => {
event.preventDefault();
document.getElementById(error.id)?.focus();
}}
>
{error.message}
</a>
</li>
))}
</ul>
</AlertDescription>
</Alert>
)}
{saved && (
<Alert role="status">
<CircleCheckIcon aria-hidden="true" className="text-success!" />
<AlertTitle>Billing details saved</AlertTitle>
<AlertDescription>
Your next invoice will use these details.
</AlertDescription>
</Alert>
)}
{fields.map((field) => {
const message = errorFor(field.id);
return (
<div key={field.id} className="grid gap-2">
<Label htmlFor={field.id}>{field.label}</Label>
<Input
id={field.id}
autoComplete={field.autoComplete}
value={values[field.name]}
aria-invalid={message ? true : undefined}
aria-describedby={message ? `${field.id}-error` : undefined}
onChange={(event) => {
setSaved(false);
setValues((current) => ({
...current,
[field.name]: event.target.value,
}));
}}
/>
{message && (
<p id={`${field.id}-error`} className="text-xs text-destructive">
{message}
</p>
)}
</div>
);
})}
<Button type="submit" className="w-full sm:w-auto sm:justify-self-end">
Save details
</Button>
</form>
);
}
npx shadcn@latest add @sevenui/component/alert-07pnpm dlx shadcn@latest add @sevenui/component/alert-07yarn dlx shadcn@latest add @sevenui/component/alert-07bunx --bun shadcn@latest add @sevenui/component/alert-073 of 120 rows were skipped
The rest of customers.csv imported successfully.
"use client";
import * as React from "react";
import { cn } from "cn";
import { ChevronDownIcon, FileWarningIcon } from "lucide-react";
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
const issues = [
{ row: 14, field: "email", message: "“jordan@” is not a valid address" },
{ row: 37, field: "plan", message: "“Enterprize” does not match a plan" },
{ row: 52, field: "seats", message: "Must be a whole number, got 2.5" },
];
export default function Alert08() {
const [open, setOpen] = React.useState(false);
return (
<Alert variant="destructive" className="w-full max-w-md">
<FileWarningIcon aria-hidden="true" />
<AlertTitle>3 of 120 rows were skipped</AlertTitle>
<AlertDescription>
<div>The rest of customers.csv imported successfully.</div>
<Collapsible open={open} onOpenChange={setOpen}>
<CollapsibleTrigger className="-mx-1 mt-1 inline-flex items-center gap-1 rounded-sm px-1 font-medium text-destructive outline-none hover:underline focus-visible:ring-3 focus-visible:ring-destructive/30">
{open ? "Hide details" : "Show details"}
<ChevronDownIcon
aria-hidden="true"
className={cn(
"size-3.5 transition-transform duration-200",
open && "rotate-180",
)}
/>
</CollapsibleTrigger>
<CollapsibleContent>
<ul className="mt-2 divide-y divide-destructive/15 rounded-md border border-destructive/20 text-xs">
{issues.map((issue) => (
<li key={issue.row} className="grid gap-0.5 px-2.5 py-2">
<span className="font-medium tabular-nums">
Row {issue.row} ·{" "}
<code className="font-mono">{issue.field}</code>
</span>
<span>{issue.message}</span>
</li>
))}
</ul>
</CollapsibleContent>
</Collapsible>
</AlertDescription>
</Alert>
);
}
npx shadcn@latest add @sevenui/component/alert-08pnpm dlx shadcn@latest add @sevenui/component/alert-08yarn dlx shadcn@latest add @sevenui/component/alert-08bunx --bun shadcn@latest add @sevenui/component/alert-08Restore point from 08:00 is ready
Restoring replaces 1,284 records changed since this morning.
"use client";
import * as React from "react";
import { CircleCheckIcon, DatabaseBackupIcon } from "lucide-react";
import {
Alert,
AlertAction,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Progress } from "@/components/ui/progress";
import { Spinner } from "@/components/ui/spinner";
type Phase = "idle" | "running" | "done";
export default function Alert09() {
const [phase, setPhase] = React.useState<Phase>("idle");
const [value, setValue] = React.useState(0);
React.useEffect(() => {
if (phase !== "running") return;
const id = window.setInterval(() => {
setValue((current) => Math.min(current + 8, 100));
}, 250);
return () => window.clearInterval(id);
}, [phase]);
React.useEffect(() => {
if (phase === "running" && value >= 100) setPhase("done");
}, [phase, value]);
function start() {
setValue(0);
setPhase("running");
}
return (
<div className="w-full max-w-md">
{phase === "idle" && (
<Alert role="status">
<DatabaseBackupIcon aria-hidden="true" />
<AlertTitle>Restore point from 08:00 is ready</AlertTitle>
<AlertDescription>
Restoring replaces 1,284 records changed since this morning.
</AlertDescription>
<AlertAction>
<Button size="xs" onClick={start}>
Restore
</Button>
</AlertAction>
</Alert>
)}
{phase === "running" && (
<Alert role="status" aria-busy="true">
<Spinner aria-hidden="true" />
<AlertTitle>Restoring your data</AlertTitle>
<AlertDescription className="grid gap-2">
<span className="tabular-nums">
{Math.round((value / 100) * 1284).toLocaleString("en-US")} of
1,284 records
</span>
<Progress value={value} aria-label="Restore progress" />
</AlertDescription>
</Alert>
)}
{phase === "done" && (
<Alert role="status" className="border-success/30 bg-success/5">
<CircleCheckIcon aria-hidden="true" className="text-success!" />
<AlertTitle>Restore complete</AlertTitle>
<AlertDescription>
All 1,284 records match the 08:00 restore point.
</AlertDescription>
<AlertAction>
<Button size="xs" variant="outline" onClick={() => setPhase("idle")}>
Done
</Button>
</AlertAction>
</Alert>
)}
</div>
);
}
npx shadcn@latest add @sevenui/component/alert-09pnpm dlx shadcn@latest add @sevenui/component/alert-09yarn dlx shadcn@latest add @sevenui/component/alert-09bunx --bun shadcn@latest add @sevenui/component/alert-09Elevated API latency in eu-west
Requests are slower than usual. Engineers are investigating.
1/3
"use client";
import * as React from "react";
import {
ChevronLeftIcon,
ChevronRightIcon,
KeyRoundIcon,
ServerCrashIcon,
UsersIcon,
} from "lucide-react";
import {
Alert,
AlertAction,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
const notices = [
{
id: "incident",
icon: ServerCrashIcon,
title: "Elevated API latency in eu-west",
description: "Requests are slower than usual. Engineers are investigating.",
},
{
id: "keys",
icon: KeyRoundIcon,
title: "2 API keys expire in 7 days",
description: "Rotate the production and staging keys before September 30.",
},
{
id: "seats",
icon: UsersIcon,
title: "All 25 seats are in use",
description: "New invites will wait for approval until you add seats.",
},
];
export default function Alert10() {
const [index, setIndex] = React.useState(0);
const notice = notices[index];
const Icon = notice.icon;
const remaining = notices.length - 1 - index;
return (
<section
aria-label="Account notices"
className="relative w-full max-w-md pb-3"
>
{remaining > 0 && (
<div
aria-hidden="true"
className="absolute inset-x-3 bottom-1.5 h-4 rounded-lg border bg-card"
/>
)}
{remaining > 1 && (
<div
aria-hidden="true"
className="absolute inset-x-6 bottom-0 h-4 rounded-lg border bg-card"
/>
)}
<Alert
key={notice.id}
role="status"
className="relative z-10 pr-24! shadow-sm"
>
<Icon aria-hidden="true" />
<AlertTitle>{notice.title}</AlertTitle>
<AlertDescription>{notice.description}</AlertDescription>
<AlertAction className="flex items-center gap-0.5">
<Button
variant="ghost"
size="icon-xs"
aria-label="Previous notice"
disabled={index === 0}
onClick={() => setIndex((i) => i - 1)}
>
<ChevronLeftIcon aria-hidden="true" />
</Button>
<span className="min-w-8 text-center text-xs text-muted-foreground tabular-nums">
{index + 1}/{notices.length}
</span>
<Button
variant="ghost"
size="icon-xs"
aria-label="Next notice"
disabled={index === notices.length - 1}
onClick={() => setIndex((i) => i + 1)}
>
<ChevronRightIcon aria-hidden="true" />
</Button>
</AlertAction>
</Alert>
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-10pnpm dlx shadcn@latest add @sevenui/component/alert-10yarn dlx shadcn@latest add @sevenui/component/alert-10bunx --bun shadcn@latest add @sevenui/component/alert-10"use client";
import * as React from "react";
import { CreditCardIcon, RefreshCwIcon } from "lucide-react";
import {
Alert,
AlertAction,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
const plan = {
name: "Team plan",
price: "$96.00",
cycle: "Billed monthly for 8 seats",
card: "Visa ending in 4242",
failedOn: "Sep 22",
retryOn: "Sep 29",
};
export default function Alert11() {
const [retried, setRetried] = React.useState(false);
return (
<Card className="w-full max-w-md">
<CardHeader>
<CardTitle>{plan.name}</CardTitle>
<CardDescription>{plan.cycle}</CardDescription>
</CardHeader>
<CardContent className="grid gap-4">
{retried ? (
<Alert aria-live="polite">
<RefreshCwIcon aria-hidden="true" />
<AlertTitle>Retry scheduled</AlertTitle>
<AlertDescription>
We will charge {plan.card} again within the next hour and email
the receipt to billing@northwind.io.
</AlertDescription>
<AlertAction>
<Button size="xs" variant="ghost" onClick={() => setRetried(false)}>
Cancel
</Button>
</AlertAction>
</Alert>
) : (
<Alert variant="destructive">
<CreditCardIcon aria-hidden="true" />
<AlertTitle>Your {plan.failedOn} payment was declined</AlertTitle>
<AlertDescription>
<p>
The bank declined the {plan.price} charge to {plan.card}. Update
your card before {plan.retryOn} to keep all 8 seats active.
</p>
</AlertDescription>
<div className="col-start-2 mt-2 flex flex-wrap gap-2">
<Button size="sm">Update payment method</Button>
<Button size="sm" variant="outline" onClick={() => setRetried(true)}>
Retry charge
</Button>
</div>
</Alert>
)}
<dl className="grid grid-cols-2 gap-y-2 text-sm">
<dt className="text-muted-foreground">Amount due</dt>
<dd className="text-right font-medium tabular-nums">{plan.price}</dd>
<dt className="text-muted-foreground">Next attempt</dt>
<dd className="text-right tabular-nums">{plan.retryOn}</dd>
</dl>
</CardContent>
<CardFooter className="border-t text-xs text-muted-foreground">
Seats become read-only if the balance stays unpaid for 14 days.
</CardFooter>
</Card>
);
}
npx shadcn@latest add @sevenui/component/alert-11pnpm dlx shadcn@latest add @sevenui/component/alert-11yarn dlx shadcn@latest add @sevenui/component/alert-11bunx --bun shadcn@latest add @sevenui/component/alert-11"use client";
import * as React from "react";
import { CircleDotIcon } from "lucide-react";
import {
Alert,
AlertAction,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const saved = {
name: "Daniel Okafor",
email: "daniel@lumen.studio",
title: "Product designer",
};
type Profile = typeof saved;
export default function Alert12() {
const [committed, setCommitted] = React.useState<Profile>(saved);
const [draft, setDraft] = React.useState<Profile>({
...saved,
title: "Senior product designer",
});
const changed = (Object.keys(draft) as (keyof Profile)[]).filter(
(key) => draft[key] !== committed[key],
);
const update =
(key: keyof Profile) => (event: React.ChangeEvent<HTMLInputElement>) =>
setDraft((current) => ({ ...current, [key]: event.target.value }));
return (
<form
className="grid w-full max-w-md gap-4"
onSubmit={(event) => {
event.preventDefault();
setCommitted(draft);
}}
>
<div className="grid gap-1">
<h3 className="font-medium">Profile</h3>
<p className="text-sm text-muted-foreground">
How teammates see you across the workspace.
</p>
</div>
<div className="grid gap-2">
<Label htmlFor="alert-12-name">Full name</Label>
<Input id="alert-12-name" value={draft.name} onChange={update("name")} />
</div>
<div className="grid gap-2">
<Label htmlFor="alert-12-email">Email</Label>
<Input
id="alert-12-email"
type="email"
value={draft.email}
onChange={update("email")}
/>
</div>
<div className="grid gap-2">
<Label htmlFor="alert-12-title">Job title</Label>
<Input
id="alert-12-title"
value={draft.title}
onChange={update("title")}
/>
</div>
<div aria-live="polite">
{changed.length > 0 ? (
<Alert className="bg-muted/60 has-data-[slot=alert-action]:pr-2.5 sm:has-data-[slot=alert-action]:pr-52">
<CircleDotIcon aria-hidden="true" className="text-primary!" />
<AlertTitle>
{changed.length} unsaved {changed.length === 1 ? "change" : "changes"}
</AlertTitle>
<AlertDescription>
Edited: {changed.join(", ")}.
</AlertDescription>
<AlertAction className="static col-start-2 mt-2 flex gap-2 sm:absolute sm:top-1/2 sm:mt-0 sm:-translate-y-1/2">
<Button
type="button"
size="sm"
variant="ghost"
onClick={() => setDraft(committed)}
>
Discard
</Button>
<Button type="submit" size="sm">
Save changes
</Button>
</AlertAction>
</Alert>
) : (
<p className="text-sm text-muted-foreground">All changes saved.</p>
)}
</div>
</form>
);
}
npx shadcn@latest add @sevenui/component/alert-12pnpm dlx shadcn@latest add @sevenui/component/alert-12yarn dlx shadcn@latest add @sevenui/component/alert-12bunx --bun shadcn@latest add @sevenui/component/alert-12Team members
5 of 5 seats- ACOwner
Ava Chen
ava@orbitlabs.dev
- JRAdmin
Jonah Reyes
jonah@orbitlabs.dev
- PNMember
Priya Nair
priya@orbitlabs.dev
- LMMember
Leo Martins
leo@orbitlabs.dev
- SPInvited
Sam Patel
sam@orbitlabs.dev
All 5 seats are in use
Remove a member or add a seat for $12 a month to invite more people.
"use client";
import * as React from "react";
import { UsersRoundIcon } from "lucide-react";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
const INCLUDED_SEATS = 5;
const initialMembers = [
{ name: "Ava Chen", email: "ava@orbitlabs.dev", role: "Owner" },
{ name: "Jonah Reyes", email: "jonah@orbitlabs.dev", role: "Admin" },
{ name: "Priya Nair", email: "priya@orbitlabs.dev", role: "Member" },
{ name: "Leo Martins", email: "leo@orbitlabs.dev", role: "Member" },
];
function initials(name: string) {
return name
.split(" ")
.map((part) => part[0])
.join("");
}
export default function Alert13() {
const [members, setMembers] = React.useState([
...initialMembers,
{ name: "Sam Patel", email: "sam@orbitlabs.dev", role: "Invited" },
]);
const [seatLimit, setSeatLimit] = React.useState(INCLUDED_SEATS);
const [email, setEmail] = React.useState("");
const [error, setError] = React.useState<string | null>(null);
const full = members.length >= seatLimit;
function invite(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const address = email.trim();
if (!address || full) return;
if (
members.some(
(member) => member.email.toLowerCase() === address.toLowerCase(),
)
) {
setError(`${address} is already on the team.`);
return;
}
setError(null);
setMembers((current) => [
...current,
{ name: address.split("@")[0], email: address, role: "Invited" },
]);
setEmail("");
}
return (
<section
aria-labelledby="alert-13-heading"
className="grid w-full max-w-md gap-4 rounded-xl border bg-card p-4"
>
<div className="flex items-baseline justify-between gap-2">
<h3 id="alert-13-heading" className="font-medium">
Team members
</h3>
<span className="text-sm text-muted-foreground tabular-nums">
{members.length} of {seatLimit} seats
</span>
</div>
<ul className="grid gap-3">
{members.map((member) => (
<li key={member.email} className="flex items-center gap-3">
<Avatar size="sm">
<AvatarFallback>{initials(member.name)}</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{member.name}</p>
<p className="truncate text-xs text-muted-foreground">
{member.email}
</p>
</div>
<span className="text-xs text-muted-foreground">{member.role}</span>
</li>
))}
</ul>
{full ? (
<Alert aria-live="polite">
<UsersRoundIcon aria-hidden="true" />
<AlertTitle>All {seatLimit} seats are in use</AlertTitle>
<AlertDescription>
Remove a member or add a seat for $12 a month to invite
more people.
</AlertDescription>
<div className="col-start-2 mt-2 flex flex-wrap gap-2">
<Button size="sm" onClick={() => setSeatLimit((limit) => limit + 1)}>
Add a seat
</Button>
<Button
size="sm"
variant="ghost"
disabled={!members.some((member) => member.role === "Invited")}
onClick={() =>
setMembers((current) => {
const last = current
.map((member) => member.role)
.lastIndexOf("Invited");
return current.filter((_, index) => index !== last);
})
}
>
Remove last invite
</Button>
</div>
</Alert>
) : (
<form onSubmit={invite} className="grid gap-2">
<Label htmlFor="alert-13-email">Invite by email</Label>
<div className="flex gap-2">
<Input
id="alert-13-email"
type="email"
placeholder="name@orbitlabs.dev"
value={email}
onChange={(event) => {
setEmail(event.target.value);
setError(null);
}}
aria-invalid={error ? true : undefined}
aria-describedby="alert-13-hint"
required
/>
<Button type="submit">Invite</Button>
</div>
<p
id="alert-13-hint"
aria-live="polite"
className={
error ? "text-xs text-destructive" : "text-xs text-muted-foreground"
}
>
{error ??
`${seatLimit - members.length} seat${
seatLimit - members.length === 1 ? "" : "s"
} left on your plan.`}
</p>
</form>
)}
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-13pnpm dlx shadcn@latest add @sevenui/component/alert-13yarn dlx shadcn@latest add @sevenui/component/alert-13bunx --bun shadcn@latest add @sevenui/component/alert-13Production API key created
Name: checkout-service · Scope: payments:write
Copy this key now
For your security we only show it once. Store it in your secrets manager; if you lose it, roll a new key.
ak_live_51Hx9QeLmT7vRk2aPz0cN8wYbF3uJd"use client";
import * as React from "react";
import { CheckIcon, CopyIcon, KeyRoundIcon, LockIcon } from "lucide-react";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
const firstSecret = "ak_live_51Hx9QeLmT7vRk2aPz0cN8wYbF3uJd";
const alphabet =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
function rollSecret() {
let body = "";
for (let i = 0; i < 30; i++) {
body += alphabet[Math.floor(Math.random() * alphabet.length)];
}
return `ak_live_${body}`;
}
export default function Alert14() {
const [copied, setCopied] = React.useState(false);
const [stored, setStored] = React.useState(false);
const [secret, setSecret] = React.useState(firstSecret);
const timeout = React.useRef<ReturnType<typeof setTimeout> | null>(null);
React.useEffect(() => {
return () => {
if (timeout.current) clearTimeout(timeout.current);
};
}, []);
async function copy() {
try {
await navigator.clipboard.writeText(secret);
} catch {
// Clipboard access can be blocked; the key stays selectable below.
}
setCopied(true);
if (timeout.current) clearTimeout(timeout.current);
timeout.current = setTimeout(() => setCopied(false), 2000);
}
return (
<section
aria-labelledby="alert-14-heading"
className="grid w-full max-w-md gap-4 rounded-xl border bg-card p-4"
>
<div className="grid gap-1">
<h3 id="alert-14-heading" className="font-medium">
Production API key created
</h3>
<p className="text-sm text-muted-foreground">
Name: checkout-service · Scope: payments:write
</p>
</div>
{stored ? (
<Alert role="status">
<LockIcon aria-hidden="true" />
<AlertTitle>Key hidden for good</AlertTitle>
<AlertDescription>
It now shows as{" "}
<code className="font-mono text-xs whitespace-nowrap">
ak_live_…{secret.slice(-4)}
</code>
. Roll a new key if you ever lose it.
</AlertDescription>
</Alert>
) : (
<Alert className="bg-muted/50">
<KeyRoundIcon aria-hidden="true" />
<AlertTitle>Copy this key now</AlertTitle>
<AlertDescription>
For your security we only show it once. Store it in your secrets
manager; if you lose it, roll a new key.
</AlertDescription>
<div className="col-start-2 mt-3 flex min-w-0 items-center gap-1 rounded-md border bg-background py-1 pr-1 pl-2.5">
<code className="min-w-0 flex-1 truncate font-mono text-xs select-all">
{secret}
</code>
<Button
size="icon-sm"
variant="ghost"
onClick={copy}
aria-label={copied ? "Copied" : "Copy API key"}
>
{copied ? <CheckIcon aria-hidden="true" /> : <CopyIcon aria-hidden="true" />}
</Button>
</div>
<span className="sr-only" aria-live="polite">
{copied ? "API key copied to clipboard" : ""}
</span>
</Alert>
)}
<div className="flex justify-end">
{stored ? (
<Button
variant="outline"
onClick={() => {
setSecret(rollSecret());
setStored(false);
}}
>
Roll a new key
</Button>
) : (
<Button
onClick={() => {
setCopied(false);
setStored(true);
}}
>
I have saved my key
</Button>
)}
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-14pnpm dlx shadcn@latest add @sevenui/component/alert-14yarn dlx shadcn@latest add @sevenui/component/alert-14bunx --bun shadcn@latest add @sevenui/component/alert-14Quarterly roadmap sync
Tuesday, Oct 7 · 45 min · 6 guests
3 guests are busy at 2:00 PM
Dana, Marcus, and Ines have “Design review” until 3:00 PM. Pick a time when everyone is free.
Start time
"use client";
import * as React from "react";
import { CalendarCheckIcon, CalendarClockIcon, CheckIcon } from "lucide-react";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
const requested = "2:00 PM";
const openSlots = ["3:30 PM", "4:00 PM", "4:30 PM"];
export default function Alert15() {
const [time, setTime] = React.useState(requested);
const [sent, setSent] = React.useState(false);
const conflict = time === requested;
return (
<section
aria-labelledby="alert-15-heading"
className="grid w-full max-w-md gap-4 rounded-xl border bg-card p-4"
>
<div className="grid gap-1">
<h3 id="alert-15-heading" className="font-medium">
Quarterly roadmap sync
</h3>
<p className="text-sm text-muted-foreground">
Tuesday, Oct 7 · 45 min · 6 guests
</p>
</div>
<div aria-live="polite">
{conflict ? (
<Alert variant="destructive">
<CalendarClockIcon aria-hidden="true" />
<AlertTitle>3 guests are busy at {requested}</AlertTitle>
<AlertDescription>
Dana, Marcus, and Ines have “Design review” until 3:00 PM. Pick a
time when everyone is free.
</AlertDescription>
</Alert>
) : (
<Alert>
<CalendarCheckIcon aria-hidden="true" className="text-success!" />
<AlertTitle>
{sent ? `Invite sent for ${time}` : `Everyone is free at ${time}`}
</AlertTitle>
<AlertDescription>
{sent
? "All 6 guests got an email. Pick another time to send an update."
: "All 6 guests accepted similar slots this week."}
</AlertDescription>
</Alert>
)}
</div>
<div className="grid gap-2">
<p id="alert-15-slots" className="text-sm font-medium">
Start time
</p>
<ToggleGroup
aria-labelledby="alert-15-slots"
variant="outline"
size="sm"
className="flex-wrap"
value={[time]}
onValueChange={(value) => {
if (!value[0] || value[0] === time) return;
setTime(value[0] as string);
setSent(false);
}}
>
{[requested, ...openSlots].map((slot) => (
<ToggleGroupItem
key={slot}
value={slot}
className="tabular-nums"
>
{slot}
{slot === requested ? (
<span className="sr-only"> (conflict)</span>
) : null}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<Button disabled={conflict || sent} onClick={() => setSent(true)}>
{sent ? (
<>
<CheckIcon aria-hidden="true" data-icon="inline-start" />
Invite sent
</>
) : (
"Send invite"
)}
</Button>
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-15pnpm dlx shadcn@latest add @sevenui/component/alert-15yarn dlx shadcn@latest add @sevenui/component/alert-15bunx --bun shadcn@latest add @sevenui/component/alert-15Connect harborcoffee.com
Add these records at your DNS provider, then check the connection.
| Type | Host | Value |
|---|---|---|
| TXT | _verify | sevn-site-verification=8f3a2c91 |
| CNAME | www | edge.acme-hosting.net |
Waiting for DNS records
Most providers publish changes within 10 minutes; some take up to 48 hours.
"use client";
import * as React from "react";
import { CircleCheckIcon, GlobeIcon, TriangleAlertIcon } from "lucide-react";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
const records = [
{ type: "TXT", host: "_verify", value: "sevn-site-verification=8f3a2c91" },
{ type: "CNAME", host: "www", value: "edge.acme-hosting.net" },
];
type Status = "pending" | "checking" | "failed" | "verified";
export default function Alert16() {
const [status, setStatus] = React.useState<Status>("pending");
const [attempts, setAttempts] = React.useState(0);
React.useEffect(() => {
if (status !== "checking") return;
// The first lookup misses so the failure path is visible; the second passes.
const timer = setTimeout(
() => setStatus(attempts > 1 ? "verified" : "failed"),
1400,
);
return () => clearTimeout(timer);
}, [status, attempts]);
function check() {
setAttempts((count) => count + 1);
setStatus("checking");
}
return (
<section
aria-labelledby="alert-16-heading"
className="grid w-full max-w-md gap-4 rounded-xl border bg-card p-4"
>
<div className="grid gap-1">
<h3 id="alert-16-heading" className="font-medium">
Connect harborcoffee.com
</h3>
<p className="text-sm text-muted-foreground">
Add these records at your DNS provider, then check the connection.
</p>
</div>
<div className="overflow-hidden rounded-lg border">
<table className="w-full table-fixed text-left text-xs">
<thead className="bg-muted/60 text-muted-foreground">
<tr>
<th scope="col" className="w-16 px-2.5 py-1.5 font-medium">
Type
</th>
<th scope="col" className="w-20 px-2.5 py-1.5 font-medium">
Host
</th>
<th scope="col" className="px-2.5 py-1.5 font-medium">
Value
</th>
</tr>
</thead>
<tbody className="font-mono">
{records.map((record) => (
<tr key={record.type} className="border-t">
<td className="px-2.5 py-2">{record.type}</td>
<td className="px-2.5 py-2">{record.host}</td>
<td className="px-2.5 py-2 break-all select-all" title={record.value}>
{record.value}
</td>
</tr>
))}
</tbody>
</table>
</div>
<div aria-live="polite">
{status === "pending" && (
<Alert>
<GlobeIcon aria-hidden="true" />
<AlertTitle>Waiting for DNS records</AlertTitle>
<AlertDescription>
Most providers publish changes within 10 minutes; some take up to
48 hours.
</AlertDescription>
</Alert>
)}
{status === "checking" && (
<Alert>
<Spinner aria-hidden="true" role="presentation" />
<AlertTitle>Looking up harborcoffee.com…</AlertTitle>
<AlertDescription>Querying public resolvers.</AlertDescription>
</Alert>
)}
{status === "failed" && (
<Alert variant="destructive">
<TriangleAlertIcon aria-hidden="true" />
<AlertTitle>TXT record not found yet</AlertTitle>
<AlertDescription>
We found the CNAME but not _verify. Check the host name has no
trailing domain, then try again.
</AlertDescription>
</Alert>
)}
{status === "verified" && (
<Alert>
<CircleCheckIcon aria-hidden="true" className="text-success!" />
<AlertTitle>harborcoffee.com is connected</AlertTitle>
<AlertDescription>
HTTPS certificate issued. Your site is live on the new domain.
</AlertDescription>
</Alert>
)}
</div>
<div className="flex justify-end gap-2">
{status === "verified" ? (
<Button variant="outline">
Continue to site settings
</Button>
) : (
<Button onClick={check} disabled={status === "checking"}>
{status === "failed" ? "Check again" : "Verify records"}
</Button>
)}
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-16pnpm dlx shadcn@latest add @sevenui/component/alert-16yarn dlx shadcn@latest add @sevenui/component/alert-16bunx --bun shadcn@latest add @sevenui/component/alert-16System status
Updated 1 min agoWebhook deliveries are delayed
Events are not lost; they arrive late while the queue recovers. API requests are unaffected.
- MonitoringA fix is deployed. Queued webhooks are draining; backlog is at 12%.
- IdentifiedA saturated queue worker in eu-west-1 is delaying deliveries.
- InvestigatingWe are seeing delayed webhook deliveries for some accounts.
- API84 msOperational
- Webhooks2.4 sDegraded
- Dashboard120 msOperational
"use client";
import * as React from "react";
import { ActivityIcon, BellRingIcon } from "lucide-react";
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Badge } from "@/components/ui/badge";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
const services = [
{ name: "API", latency: "84 ms", status: "Operational" },
{ name: "Webhooks", latency: "2.4 s", status: "Degraded" },
{ name: "Dashboard", latency: "120 ms", status: "Operational" },
];
const updates = [
{
time: "14:52 UTC",
stage: "Monitoring",
body: "A fix is deployed. Queued webhooks are draining; backlog is at 12%.",
},
{
time: "14:31 UTC",
stage: "Identified",
body: "A saturated queue worker in eu-west-1 is delaying deliveries.",
},
{
time: "14:10 UTC",
stage: "Investigating",
body: "We are seeing delayed webhook deliveries for some accounts.",
},
];
export default function Alert17() {
const [subscribed, setSubscribed] = React.useState(false);
return (
<section
aria-labelledby="alert-17-heading"
className="grid w-full max-w-md gap-4 rounded-xl border bg-card p-4"
>
<div className="flex items-center justify-between gap-2">
<h3 id="alert-17-heading" className="font-medium">
System status
</h3>
<span className="text-xs text-muted-foreground">Updated 1 min ago</span>
</div>
<Alert className="border-warning/40 bg-warning/10">
<ActivityIcon aria-hidden="true" className="text-warning!" />
<AlertTitle>Webhook deliveries are delayed</AlertTitle>
<AlertDescription className="text-foreground/80">
Events are not lost; they arrive late while the queue recovers. API
requests are unaffected.
</AlertDescription>
<ol className="col-start-2 mt-3 grid gap-3 border-l pl-3">
{updates.map((update, index) => (
<li key={update.time} className="grid gap-0.5 text-xs">
<span className="flex flex-wrap items-center gap-x-2">
<span
className={
index === 0 ? "font-medium text-foreground" : "font-medium"
}
>
{update.stage}
</span>
<time className="text-muted-foreground tabular-nums">
{update.time}
</time>
</span>
<span className="text-muted-foreground">{update.body}</span>
</li>
))}
</ol>
</Alert>
<ul className="divide-y rounded-lg border">
{services.map((service) => (
<li
key={service.name}
className="flex items-center gap-3 px-3 py-2 text-sm"
>
<span className="min-w-0 flex-1 truncate font-medium">{service.name}</span>
<span className="shrink-0 text-muted-foreground tabular-nums">
{service.latency}
</span>
<Badge variant={service.status === "Degraded" ? "outline" : "secondary"}>
{service.status}
</Badge>
</li>
))}
</ul>
<div className="flex items-center justify-between gap-3">
<Label htmlFor="alert-17-subscribe" className="flex items-center gap-2 font-normal">
<BellRingIcon aria-hidden="true" className="size-4 text-muted-foreground" />
Email me when this is resolved
</Label>
<Switch
id="alert-17-subscribe"
checked={subscribed}
onCheckedChange={setSubscribed}
/>
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-17pnpm dlx shadcn@latest add @sevenui/component/alert-17yarn dlx shadcn@latest add @sevenui/component/alert-17bunx --bun shadcn@latest add @sevenui/component/alert-17Notifications
- New sign-in from Lisbon, Portugal
Chrome on Windows at 09:14. If this was not you, reset your password.
12 min ago - 2 admins have no two-factor authentication
Require 2FA for admins to protect workspace settings.
2 h ago - Invoice INV-2041 is ready
$1,240.00 for September, due Oct 15.
Yesterday - Sofia Alvarez accepted your invite
She joined Marketing with editor access.
Mon
"use client";
import * as React from "react";
import {
BellOffIcon,
CreditCardIcon,
LogInIcon,
ShieldAlertIcon,
UserPlusIcon,
XIcon,
} from "lucide-react";
import {
Alert,
AlertAction,
AlertDescription,
AlertTitle,
} from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/components/ui/tabs";
type Category = "security" | "billing" | "team";
type Notice = {
id: string;
category: Category;
icon: typeof LogInIcon;
urgent?: boolean;
title: string;
body: string;
time: string;
cta: string;
};
const initialNotices: Notice[] = [
{
id: "login",
category: "security",
icon: LogInIcon,
urgent: true,
title: "New sign-in from Lisbon, Portugal",
body: "Chrome on Windows at 09:14. If this was not you, reset your password.",
time: "12 min ago",
cta: "Review activity",
},
{
id: "2fa",
category: "security",
icon: ShieldAlertIcon,
title: "2 admins have no two-factor authentication",
body: "Require 2FA for admins to protect workspace settings.",
time: "2 h ago",
cta: "Enforce 2FA",
},
{
id: "invoice",
category: "billing",
icon: CreditCardIcon,
title: "Invoice INV-2041 is ready",
body: "$1,240.00 for September, due Oct 15.",
time: "Yesterday",
cta: "Download PDF",
},
{
id: "invite",
category: "team",
icon: UserPlusIcon,
title: "Sofia Alvarez accepted your invite",
body: "She joined Marketing with editor access.",
time: "Mon",
cta: "View profile",
},
];
const tabs: { value: "all" | Category; label: string }[] = [
{ value: "all", label: "All" },
{ value: "security", label: "Security" },
{ value: "billing", label: "Billing" },
{ value: "team", label: "Team" },
];
export default function Alert18() {
const [notices, setNotices] = React.useState(initialNotices);
const [lastDismissed, setLastDismissed] = React.useState<{
notice: Notice;
index: number;
} | null>(null);
function dismiss(id: string) {
const index = notices.findIndex((notice) => notice.id === id);
if (index === -1) return;
setLastDismissed({ notice: notices[index], index });
setNotices((current) => current.filter((notice) => notice.id !== id));
}
function undo() {
if (!lastDismissed) return;
setNotices((current) => {
const next = [...current];
next.splice(lastDismissed.index, 0, lastDismissed.notice);
return next;
});
setLastDismissed(null);
}
return (
<section
aria-labelledby="alert-18-heading"
className="grid w-full max-w-md gap-3 rounded-xl border bg-card p-4"
>
<div className="flex items-center justify-between gap-2">
<h3 id="alert-18-heading" className="font-medium">
Notifications
</h3>
<Button
variant="ghost"
size="sm"
disabled={notices.length === 0}
onClick={() => {
setNotices([]);
setLastDismissed(null);
}}
>
Clear all
</Button>
</div>
<Tabs defaultValue="all" className="gap-3">
<TabsList className="w-full">
{tabs.map((tab) => {
const count = notices.filter(
(notice) => tab.value === "all" || notice.category === tab.value,
).length;
return (
<TabsTrigger
key={tab.value}
value={tab.value}
className="gap-1 px-1 text-xs sm:gap-1.5 sm:px-2 sm:text-sm"
>
{tab.label}
{count > 0 ? (
<span className="text-muted-foreground tabular-nums">
{count}
</span>
) : null}
</TabsTrigger>
);
})}
</TabsList>
{tabs.map((tab) => {
const list = notices.filter(
(notice) => tab.value === "all" || notice.category === tab.value,
);
return (
<TabsContent key={tab.value} value={tab.value}>
{list.length === 0 ? (
<div className="grid justify-items-center gap-1 rounded-lg border border-dashed px-4 py-8 text-center">
<BellOffIcon
aria-hidden="true"
className="mb-1 size-5 text-muted-foreground"
/>
<p className="font-medium">You are all caught up</p>
<p className="text-muted-foreground">
New {tab.value === "all" ? "" : `${tab.label.toLowerCase()} `}
notifications will show up here.
</p>
</div>
) : (
<ul className="grid gap-2">
{list.map((notice) => (
<li key={notice.id}>
<Alert
role="group"
aria-label={notice.title}
variant={notice.urgent ? "destructive" : "default"}
className="has-data-[slot=alert-action]:pr-10"
>
<notice.icon aria-hidden="true" />
<AlertTitle>{notice.title}</AlertTitle>
<AlertDescription>
<p>{notice.body}</p>
</AlertDescription>
<div className="col-start-2 mt-2 flex items-center justify-between gap-2">
<Button size="xs" variant="outline">
{notice.cta}
</Button>
<span className="shrink-0 text-xs whitespace-nowrap text-muted-foreground">
{notice.time}
</span>
</div>
<AlertAction>
<Button
size="icon-xs"
variant="ghost"
aria-label={`Dismiss: ${notice.title}`}
onClick={() => dismiss(notice.id)}
>
<XIcon aria-hidden="true" />
</Button>
</AlertAction>
</Alert>
</li>
))}
</ul>
)}
</TabsContent>
);
})}
</Tabs>
<div aria-live="polite" className="min-h-0">
{lastDismissed ? (
<div className="flex items-center justify-between gap-2 rounded-lg bg-muted px-3 py-1.5 text-sm">
<span className="truncate text-muted-foreground">
Dismissed “{lastDismissed.notice.title}”
</span>
<Button size="xs" variant="ghost" onClick={undo}>
Undo
</Button>
</div>
) : null}
</div>
</section>
);
}
npx shadcn@latest add @sevenui/component/alert-18pnpm dlx shadcn@latest add @sevenui/component/alert-18yarn dlx shadcn@latest add @sevenui/component/alert-18bunx --bun shadcn@latest add @sevenui/component/alert-18