Skip to contentVibraUI
Feedback & status

Alert

A bordered notice with an icon, a title, a description and an action.

shadcn's base-nova alert with three changes. The action takes a column of its own at the inline end, where shadcn floats it over the last 72px of the text, so a label like "Reconnect" no longer runs over the words; under 24rem of alert a labelled action moves under the description, while an icon button keeps its corner. The recipe names no physical side, so an alert mirrors whole in right-to-left text. And the role follows the variant, as Banner's does: a default notice is a polite status, a destructive one an alert that interrupts. In Vibra's theme the destructive variant reads --destructive, which every palette points at --danger. For a notice washed in the success, warning or info tone, or a page-wide strip with a dismiss, reach for Banner; for an aside inside a card, Callout.

Install

npx shadcn@latest add @vibra/alert

Needs the @vibra registry in your components.json — set it up once.

Examples

Parts

Text alone, an icon and a line, and the full set. The icon column exists only when there is an icon.

Tone in the icon

The box and the words stay neutral and the icon carries success or warning, by its shape as much as its colour.

With a link

The fix is elsewhere in the app, so the description links to it, underlined in the alert's own tone.

With an id to copy

A failure someone else will look into: the id in the code face, and a copy button that keeps the corner.

Checklist

What still stands in the way, each line saying its state in words as well as by its mark.

In progress

A spinner in the icon's place, the count in words and a named bar; aria-busy holds the updates back.

With an action

Undo, offered where the change is reported. The button stays, focused, once it has done its work.

Approve or decline

Two actions named for the person. The answer rewrites the notice, and the other button goes.

Dismissible

A close button named for what it closes. Focus moves to the way back, and back to the close button again.

Error summary

Counts a form's problems and links to each field. Sending with problems moves the focus to it.

Right to left

The demo's alerts in Arabic: the icon at the inline start, the action at the inline end.

Props

PropTypeDefaultDescription
variant"default" | "destructive""default"destructive sets the text and icon in the danger tone.
roleReact.AriaRole | undefined"status", or "alert" when destructivestatus is read at the next pause, alert at once. Pass role={undefined} for a notice that was on the page before the reader arrived.
AlertActionReact.ComponentProps<"div">—A button or two at the inline end, beside the title. Under 24rem of alert a labelled action moves under the description; an icon button stays in the corner.

Dependencies

Source

components/ui/alert.tsx
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"

// An action takes a column of its own at the inline end, so a label of any
// length is laid out beside the text rather than over it; under 24rem of alert
// (a phone, a narrow card) that column would starve the text, so a labelled
// action drops under the description instead, while an icon button — a
// dismiss, a copy — keeps its corner. The alert is a size container only when
// it holds an action, so an alert sized by its content still has a width.
const alertVariants = cva(
  "group/alert relative grid w-full grid-cols-[1fr] gap-0.5 rounded-lg border px-2.5 py-2 text-start text-sm has-data-[slot=alert-action]:@container/alert has-data-[slot=alert-action]:gap-x-2 has-[>svg]:grid-cols-[auto_1fr] has-[>svg]:gap-x-2 *:[svg]:row-span-2 *:[svg]:translate-y-0.5 *:[svg]:text-current *:[svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-card text-card-foreground",
        destructive:
          "bg-card text-destructive *:data-[slot=alert-description]:text-destructive/90 *:[svg]:text-current",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

function Alert({
  className,
  variant = "default",
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
  return (
    <div
      data-slot="alert"
      data-variant={variant}
      // A failure interrupts; a notice waits for a pause in the reading — the
      // split Banner makes. A caller's own role wins, `undefined` included,
      // for a notice that was on the page before the reader arrived.
      role={variant === "destructive" ? "alert" : "status"}
      className={cn(alertVariants({ variant }), className)}
      {...props}
    />
  )
}

function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="alert-title"
      className={cn(
        "font-medium group-has-[>svg]/alert:col-start-2 [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground",
        className
      )}
      {...props}
    />
  )
}

function AlertDescription({
  className,
  ...props
}: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="alert-description"
      className={cn(
        "text-sm text-balance text-muted-foreground md:text-pretty [&_a]:underline [&_a]:underline-offset-3 [&_a]:hover:text-foreground [&_p:not(:last-child)]:mb-4",
        className
      )}
      {...props}
    />
  )
}

function AlertAction({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="alert-action"
      className={cn(
        // Beside the text: the column after it, from the title's line down.
        "col-start-2 row-span-2 row-start-1 ms-1 flex flex-wrap items-center gap-1.5 self-start justify-self-end group-has-[>svg]/alert:col-start-3",
        // Under it, in the text's own column, when the alert is narrow and
        // the action has a label (Button writes its size to data-size).
        "@max-sm/alert:not-has-[>[data-size^=icon]]:col-start-1 @max-sm/alert:not-has-[>[data-size^=icon]]:row-span-1 @max-sm/alert:not-has-[>[data-size^=icon]]:row-start-auto @max-sm/alert:not-has-[>[data-size^=icon]]:ms-0 @max-sm/alert:not-has-[>[data-size^=icon]]:mt-1.5 @max-sm/alert:not-has-[>[data-size^=icon]]:justify-self-start @max-sm/alert:not-has-[>[data-size^=icon]]:group-has-[>svg]/alert:col-start-2",
        className
      )}
      {...props}
    />
  )
}

export { Alert, AlertTitle, AlertDescription, AlertAction }