An inline notice with a title, message and optional dismiss action.
Your workspace is ready
Export paused
pnpm dlx shadcn@latest add @latheui/alertFirst time? Run npx latheui init once to add the @latheui registry. The CLI then writes the source into your project and installs what it needs. Or add it by hand:
Install the npm dependencies.
pnpm add lucide-reactCopy the source into your project.
"use client"
import * as React from "react"
import { InfoIcon, CircleAlertIcon, XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
export type AlertProps = React.ComponentProps<"div"> & {
title: string
variant?: "info" | "danger"
onDismiss?: () => void
}
export function Alert({
title,
variant = "info",
onDismiss,
children,
className,
ref: forwardedRef,
...props
}: AlertProps) {
const Icon = variant === "danger" ? CircleAlertIcon : InfoIcon
const root = React.useRef<HTMLDivElement>(null)
const fade = React.useRef<Animation | null>(null)
const [dismissing, setDismissing] = React.useState(false)
React.useEffect(() => () => fade.current?.cancel(), [])
const dismiss = () => {
if (dismissing || !onDismiss) return
if (!root.current || window.matchMedia("(prefers-reduced-motion: reduce)").matches) { onDismiss(); return }
setDismissing(true)
fade.current = root.current.animate([{ opacity: getComputedStyle(root.current).opacity }, { opacity: 0 }], {
duration: 200, easing: "ease-out", fill: "forwards",
})
fade.current.onfinish = () => onDismiss()
}
return (
<div
ref={node => { root.current = node; if (typeof forwardedRef === "function") forwardedRef(node); else if (forwardedRef) forwardedRef.current = node }}
data-slot="alert"
data-dismissing={dismissing || undefined}
role={variant === "danger" ? "alert" : "status"}
className={cn(
"flex items-start gap-3 rounded-lg border border-border bg-card p-4 text-sm",
variant === "danger" && "border-destructive/30 bg-destructive/5",
className
)}
{...props}
>
<Icon
aria-hidden="true"
className={cn(
"mt-0.5 size-4 shrink-0",
variant === "danger" ? "text-destructive" : "text-muted-foreground"
)}
/>
<div className="min-w-0 flex-1">
<p className="font-medium">{title}</p>
{children && (
<div className="mt-1 text-muted-foreground">{children}</div>
)}
</div>
{onDismiss && (
<button
type="button"
aria-label={"Dismiss " + title}
onClick={dismiss}
disabled={dismissing}
className="grid size-6 shrink-0 cursor-pointer place-items-center rounded-sm outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring"
>
<XIcon aria-hidden="true" className="size-4" />
</button>
)}
</div>
)
}Depends on
| Prop | Type | Default | Description |
|---|---|---|---|
| title* (required) | string | — | Notice heading. |
| variant | "info" | "danger" | "info" | Danger notices are announced assertively. |
| children | ReactNode | — | Message or action content. |
| onDismiss | () => void | — | Shows a named dismiss button. Called after a 200ms fade; immediate with reduced motion. |
Combine the control with your own layout. Semantic colors follow the active theme.
import * as React from "react"
import { AnimatePresence, motion } from "motion/react"
import { Alert } from "@/components/ui/alert"
import { ease, useMotionSafe } from "@/lib/motion"
const notices = [
{
id: "ready",
title: "Your workspace is ready",
body: "Invite collaborators whenever you are ready.",
variant: "info" as const,
},
{
id: "paused",
title: "Export paused",
body: "Check your connection and try again.",
variant: "danger" as const,
},
]
export default function Demo() {
const [open, setOpen] = React.useState(notices.map((notice) => notice.id))
const { safe } = useMotionSafe()
return (
<div className="flex w-full max-w-md flex-col">
{/* A dismissed alert fades, then its space closes so the ones below slide up. */}
<AnimatePresence initial={false}>
{notices
.filter((notice) => open.includes(notice.id))
.map((notice) => (
<motion.div
key={notice.id}
exit={{ height: 0, marginBottom: 0 }}
transition={safe({ duration: 0.28, ease: ease.out })}
className="mb-3 overflow-hidden"
>
<Alert
title={notice.title}
variant={notice.variant}
onDismiss={() => setOpen((ids) => ids.filter((id) => id !== notice.id))}
>
{notice.body}
</Alert>
</motion.div>
))}
</AnimatePresence>
</div>
)
}