Alert
Static inline message block with tones + icon (icon paired with colour, never colour alone).
import { Alert }from "@/components/ui"
Specification
Displays a static, inline callout with a tone, icon and optional title, dismiss button and action.
Props (6)
Every prop this component accepts, its type, default, and how to use it.
| Prop | Type | Default | Description |
|---|---|---|---|
| tone | "info" | "success" | "warning" | "error" | "info" | Sets the icon, colors and ARIA role (error uses role="alert"). |
| title | ReactNode | — | Optional bold heading shown above the body. |
| children | ReactNode | — | The alert body message. |
| dismissible | boolean | — | Shows a close button that hides the alert. |
| onDismiss | () => void | — | Called after the user dismisses the alert. |
| action | ReactNode | — | Optional action element (e.g. a button) rendered below the body. |
Tones
Heads up
This is an informational callout.
Saved
Your changes have been saved.
Almost out of space
You’ve used 90% of your quota.
Payment failed
We couldn’t process your card.
Full source
The complete component, read from
src/components/ui/alert.tsx.alert.tsx
"use client";
import { useState, type ReactNode } from "react";
import { Info, CheckCircle2, AlertTriangle, XCircle, X, type LucideIcon } from "lucide-react";
import { cn } from "@/lib/utils";
type AlertTone = "info" | "success" | "warning" | "error";
const TONE: Record<AlertTone, { icon: LucideIcon; wrap: string; icon_: string }> = {
info: { icon: Info, wrap: "border-border bg-bg-secondary", icon_: "text-text-secondary" },
success: { icon: CheckCircle2, wrap: "border-accent-muted bg-accent-light", icon_: "text-accent" },
warning: { icon: AlertTriangle, wrap: "border-warning/30 bg-warning-light", icon_: "text-warning" },
error: { icon: XCircle, wrap: "border-error/30 bg-error-light", icon_: "text-error" },
};
interface AlertProps {
tone?: AlertTone;
title?: ReactNode;
children?: ReactNode;
dismissible?: boolean;
onDismiss?: () => void;
action?: ReactNode;
}
export function Alert({ tone = "info", title, children, dismissible, onDismiss, action }: AlertProps) {
const [open, setOpen] = useState(true);
const cfg = TONE[tone];
const Icon = cfg.icon;
if (!open) return null;
return (
<div
role={tone === "error" ? "alert" : "status"}
className={cn("flex items-start gap-3 rounded-lg border p-3.5", cfg.wrap)}
>
<Icon size={18} className={cn("mt-0.5 shrink-0", cfg.icon_)} aria-hidden />
<div className="min-w-0 flex-1">
{title && <p className="text-sm font-semibold text-text">{title}</p>}
{children && <div className={cn("text-sm text-text-secondary", title && "mt-0.5")}>{children}</div>}
{action && <div className="mt-2">{action}</div>}
</div>
{dismissible && (
<button
type="button"
onClick={() => { setOpen(false); onDismiss?.(); }}
aria-label="Dismiss"
className="-mr-1 -mt-1 shrink-0 rounded p-1 text-text-tertiary hover:text-text focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent"
>
<X size={15} aria-hidden />
</button>
)}
</div>
);
}Dependencies
To use Alert you also need the following. Copy these into your project alongside it.
Recommended folder structure
project structure
src/components/ui/ └─ alert.tsx src/lib/ └─ utils.ts
Dependency files
These supporting files aren’t components — copy each one into the matching path above.
src/lib/utils.ts
utils.ts
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
/**
* Merge class names with clsx semantics + Tailwind conflict resolution.
* Prefer this in all components.
*/
export function cn(...inputs: ClassValue[]): string {
return twMerge(clsx(inputs));
}
/**
* Concatenate class names, filtering out falsy values.
* @deprecated Prefer {@link cn} — it also resolves Tailwind class conflicts.
*/
export function cx(...args: (string | false | null | undefined)[]): string {
return args.filter(Boolean).join(" ");
}
/**
* Focus ring style helper.
* @deprecated Focus rings are now handled globally via the
* `:focus-visible` rule in `globals.css` and the `focus-visible:*`
* utilities. This remains only for backwards compatibility.
*/
export function ring(color: string = "var(--color-accent)") {
return { outlineColor: color };
}