Alert
The app's in-flow vehicle for the result of an action. The app places it where the user is already looking and owns its lifecycle. It never hides itself: feedback stays until the user has seen it.
Variants
Four variants: info is the default, then success, warning, and error. The surface and the border stay neutral — card on border in both modes. The icon carries the variant on its own, so an alert reads as a message on the page and not as a colored block. No solid fill and no shadow.
Sizes
default takes 16px padding and a 20px icon; sm takes 12px padding and a 16px icon. The text is text-sm in both. A message surface does not shrink its type. The close button is icon-sm in both.
Title and description
AlertTitle is optional — "Saved." alone is legal. The title uses Onest at medium weight, not Baloo 2, because an alert is a message and not a heading. The description drops to muted-foreground. Weight alone was not enough separation at text-sm, so the title keeps the full foreground color and the description steps back. The status icon aligns with the first line of text.
Icon
Pass icon to replace the variant icon, or icon={null} to drop it. The alert owns the icon size, the gap, and the color, so any node you pass lands at the size and the color of the current variant.
Dismissing
Pass onClose and the alert renders a ghost icon button labelled "Dismiss". The alert never hides itself. It calls onClose and the app decides what happens — set open to false, drop the alert, or retry the action first. Without onClose there is no close button and no tab stop.
Actions
A result often needs a way forward: retry the payment, undo the delete, open the trip. The alert takes no action prop — put the buttons in the children, under the description. Two actions at most, one outline and one ghost, both at sm. A third action means the message belongs somewhere bigger than an alert. The close button stays the last tab stop.
Motion
open defaults to true and drives the one animation: height and opacity on spring-settle both ways at --motion-base. The enter does not bounce, because a bounce on a height change makes the content below overshoot. The alert carries its own AnimatePresence, so the app needs no motion boilerplate to get the exit. Unmounting <Alert> directly skips the exit — AnimatePresence cannot animate its own unmount. That is the documented trade for the boilerplate you save.
Accessibility
error renders role="alert", so a screen reader interrupts with it; the other three render role="status" and wait their turn. Pass role yourself to override the mapping. The status icon is aria-hidden, so the message is announced once. Focus return after a dismissal is the app's job. The alert does no focus management: when you remove an alert whose close button holds focus, send focus back to the control that produced the alert. Text meets WCAG AA in both modes — the description is the floor at 4.73:1 — and the close button's focus ring clears 3:1 on the card surface.