# Alert

> Inline messages for status and feedback, with a neutral surface, a colored icon and a dismiss that collapses smoothly.

Docs: https://hextaui.com/docs/alert
Markdown: https://hextaui.com/docs/alert.md

```tsx title="components/examples/alert/demo.tsx"
import { IconCircleX, IconTerminal2 } from "@tabler/icons-react"

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"

export function AlertDemo() {
  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <Alert>
        <IconTerminal2 />
        <AlertTitle>Heads up</AlertTitle>
        <AlertDescription>
          You can add components to your app using the CLI.
        </AlertDescription>
      </Alert>
      <Alert variant="destructive">
        <IconCircleX />
        <AlertTitle>Payment failed</AlertTitle>
        <AlertDescription>
          Your card was declined. Update your billing details.
        </AlertDescription>
      </Alert>
    </div>
  )
}
```

## Installation

### CLI

```bash
npx shadcn@latest add https://hextaui.com/r/alert.json
```

This adds the component, the HextaUI theme tokens and any HextaUI components it depends on.

### Manual

Add the HextaUI theme tokens (https://hextaui.com/docs/installation#theme) to your global CSS if you haven't yet, then install the dependencies.

```bash
pnpm add @base-ui/react @tabler/icons-react class-variance-authority cn
```

Copy and paste the following code into your project.

```tsx title="components/ui/alert.tsx"
"use client"

import * as React from "react"
import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { IconX } from "@tabler/icons-react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

import { easeOut, easeSpring } from "@/lib/motion"

import { Button } from "@/components/ui/button"

const alertVariants = cva(
  "grid w-full grid-cols-[auto_minmax(0,1fr)_fit-content(50%)_auto] items-center rounded-lg px-4 py-3 text-sm text-card-foreground inset-ring-(length:--hairline) inset-ring-border has-[>[data-slot=alert-description]]:items-start forced-colors:border [&>svg]:col-start-1 [&>svg]:row-start-1 [&>svg]:me-3 [&>svg]:shrink-0 has-[>[data-slot=alert-description]]:[&>svg]:mt-0.5 [&>svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "[&>svg]:text-foreground",
        destructive: "[&>svg]:text-destructive",
        success: "[&>svg]:text-success",
        info: "[&>svg]:text-info",
        warning: "[&>svg]:text-warning",
      },
      appearance: {
        outline: "bg-card",
        soft: "inset-ring-transparent [&>[data-slot=alert-close]]:hover:bg-foreground/5 [&>[data-slot=alert-description]]:text-foreground/75",
      },
    },
    compoundVariants: [
      { appearance: "soft", variant: "default", className: "bg-muted" },
      {
        appearance: "soft",
        variant: "destructive",
        className: "bg-destructive/8 dark:bg-destructive/15",
      },
      {
        appearance: "soft",
        variant: "success",
        className: "bg-success/8 dark:bg-success/15",
      },
      {
        appearance: "soft",
        variant: "info",
        className: "bg-info/8 dark:bg-info/15",
      },
      {
        appearance: "soft",
        variant: "warning",
        className: "bg-warning/8 dark:bg-warning/15",
      },
    ],
    defaultVariants: {
      variant: "default",
      appearance: "outline",
    },
  }
)

type AlertVariant = NonNullable<VariantProps<typeof alertVariants>["variant"]>
type AlertAppearance = NonNullable<
  VariantProps<typeof alertVariants>["appearance"]
>

const assertiveVariants: AlertVariant[] = ["destructive", "warning"]

const AlertContext = React.createContext<{ close: () => void } | null>(null)

function useAlertContext(part: string) {
  const context = React.useContext(AlertContext)

  if (!context) {
    throw new Error(`<${part}> must be used within <Alert>.`)
  }

  return context
}

const focusableSelector =
  'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"]), [contenteditable="true"]'

function focusNearest(from: HTMLElement) {
  const candidates = Array.from(
    document.querySelectorAll<HTMLElement>(focusableSelector)
  ).filter((node) => !from.contains(node) && node.checkVisibility())

  const target =
    candidates.find(
      (node) =>
        from.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING
    ) ??
    candidates.findLast(
      (node) =>
        from.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_PRECEDING
    )

  target?.focus()
}

function dataAttributes(slot: string, extra?: Record<string, string>) {
  return { "data-slot": slot, ...extra } as Record<string, string>
}

function collapsedMargins(element: HTMLElement) {
  const parent = element.parentElement
  const hasSiblings = Boolean(
    element.previousElementSibling || element.nextElementSibling
  )
  let gap = 0

  if (parent && hasSiblings) {
    const style = getComputedStyle(parent)
    const stacksVertically =
      style.display.includes("grid") ||
      (style.display.includes("flex") &&
        style.flexDirection.startsWith("column"))

    if (stacksVertically) {
      gap = parseFloat(style.rowGap) || 0
    }
  }

  return element.previousElementSibling
    ? { marginBlockStart: `${-gap}px`, marginBlockEnd: "0px" }
    : { marginBlockStart: "0px", marginBlockEnd: `${-gap}px` }
}

function subscribe() {
  return () => {}
}

type AlertProps = useRender.ComponentProps<"div"> & {
  variant?: AlertVariant
  appearance?: AlertAppearance
  open?: boolean
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
}

function Alert({
  className,
  variant = "default",
  appearance = "outline",
  open: openProp,
  defaultOpen = true,
  onOpenChange,
  render,
  ref,
  ...props
}: AlertProps) {
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen)
  const open = openProp ?? uncontrolledOpen
  const [present, setPresent] = React.useState(open)
  const elementRef = React.useRef<HTMLDivElement>(null)

  const isHydrating = React.useSyncExternalStore(
    subscribe,
    () => false,
    () => true
  )
  const [animateIn, setAnimateIn] = React.useState(!isHydrating)

  if (open && !present) {
    setPresent(true)
  }

  React.useEffect(() => {
    const element = elementRef.current

    if (open || !present || !element) {
      return
    }

    if (typeof element.animate !== "function") {
      setPresent(false)
      return
    }

    const hadFocus = element.contains(document.activeElement)
    const reduceMotion =
      typeof window.matchMedia !== "function" ||
      window.matchMedia("(prefers-reduced-motion: reduce)").matches

    element.style.overflow = "hidden"
    element.dataset.endingStyle = ""

    const animation = element.animate(
      [
        {
          height: `${element.offsetHeight}px`,
        },
        {
          height: "0px",
          paddingBlock: "0px",
          borderBlockWidth: "0px",
          ...collapsedMargins(element),
        },
      ],
      {
        duration: reduceMotion ? 0 : 260,
        easing: easeSpring,
        fill: "forwards",
      }
    )

    const fade = element.animate(
      [
        { opacity: 1, scale: 1 },
        { opacity: 0, scale: 0.98 },
      ],
      {
        duration: reduceMotion ? 0 : 140,
        easing: easeOut,
        fill: "forwards",
      }
    )

    animation.onfinish = () => {
      if (hadFocus) {
        focusNearest(element)
      }
      setAnimateIn(true)
      setPresent(false)
    }

    return () => {
      animation.onfinish = null
      animation.cancel()
      fade.cancel()
      element.style.overflow = ""
      delete element.dataset.endingStyle
    }
  }, [open, present])

  const context = React.useMemo(
    () => ({
      close: () => {
        if (openProp === undefined) {
          setUncontrolledOpen(false)
        }
        onOpenChange?.(false)
      },
    }),
    [openProp, onOpenChange]
  )

  const element = useRender({
    defaultTagName: "div",
    render,
    ref: ref ? [elementRef, ref] : elementRef,
    enabled: present,
    props: mergeProps<"div">(
      {
        role: assertiveVariants.includes(variant) ? "alert" : "status",
        className: cn(
          alertVariants({ variant, appearance }),
          animateIn &&
            "motion-safe:animate-in motion-safe:ease-out-quint motion-safe:animation-duration-200 motion-safe:fade-in-0 motion-safe:slide-in-from-top-1",
          className
        ),
      },
      props,
      dataAttributes("alert", {
        "data-variant": variant,
        "data-appearance": appearance,
      })
    ),
  })

  return (
    <AlertContext.Provider value={context}>{element}</AlertContext.Provider>
  )
}

function AlertTitle({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "col-start-2 min-w-0 leading-5 font-medium text-pretty wrap-anywhere",
          className
        ),
      },
      props,
      dataAttributes("alert-title")
    ),
  })
}

function AlertDescription({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "col-start-2 min-w-0 leading-5 text-pretty wrap-anywhere text-muted-foreground [&_a:not([data-slot])]:text-foreground [&_a:not([data-slot])]:underline [&_a:not([data-slot])]:decoration-current/40 [&_a:not([data-slot])]:underline-offset-3 [&_a:not([data-slot])]:transition-[text-decoration-color] [&_a:not([data-slot])]:duration-150 [&_a:not([data-slot])]:hover:decoration-current [&>p:not(:last-child)]:mb-2 [[data-slot=alert-title]+&]:mt-0.5",
          className
        ),
      },
      props,
      dataAttributes("alert-description")
    ),
  })
}

function AlertAction({
  className,
  render,
  ...props
}: useRender.ComponentProps<"div">) {
  return useRender({
    defaultTagName: "div",
    render,
    props: mergeProps<"div">(
      {
        className: cn(
          "col-start-3 row-start-1 ms-3 flex h-5 items-center gap-2 self-start",
          className
        ),
      },
      props,
      dataAttributes("alert-action")
    ),
  })
}

type AlertCloseProps = Omit<
  React.ComponentProps<typeof Button>,
  "className"
> & {
  className?: string
}

function AlertClose({
  className,
  children,
  onClick,
  "aria-label": ariaLabel = "Dismiss",
  ...props
}: AlertCloseProps) {
  const { close } = useAlertContext("AlertClose")

  return (
    <Button
      variant="ghost"
      size="icon-xs"
      aria-label={ariaLabel}
      className={cn(
        "col-start-4 row-start-1 -my-0.5 ms-2 -me-1.5 self-start text-muted-foreground hover:text-foreground",
        className
      )}
      onClick={(event) => {
        onClick?.(event)
        if (!event.defaultPrevented) {
          close()
        }
      }}
      {...props}
      data-slot="alert-close"
    >
      {children ?? <IconX />}
    </Button>
  )
}

export {
  Alert,
  AlertTitle,
  AlertDescription,
  AlertAction,
  AlertClose,
  alertVariants,
}
```

```tsx title="components/ui/button.tsx"
"use client"

import * as React from "react"
import { Button as ButtonPrimitive } from "@base-ui/react/button"
import { IconAlertCircle, IconCircleCheck } from "@tabler/icons-react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"

import { Spinner } from "@/components/ui/spinner"
import {
  useButtonFeedback,
  type ButtonFeedbackOptions,
  type ButtonStatus,
} from "@/hooks/use-button-feedback"

const buttonVariants = cva(
  "group/button relative inline-flex items-center justify-center rounded-md bg-clip-padding text-sm font-medium whitespace-nowrap inset-ring-(length:--hairline) inset-ring-transparent transition-[color,background-color,box-shadow,opacity,translate,scale] duration-200 ease-out-quint outline-none select-none focus-visible:ring-3 focus-visible:ring-focus-ring focus-visible:inset-ring-ring focus-visible:outline-hidden active:duration-100 active:not-aria-[haspopup]:not-aria-busy:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-busy:cursor-progress aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:inset-ring-destructive motion-safe:active:not-aria-[haspopup]:not-aria-busy:scale-[0.97] dark:aria-invalid:ring-destructive/40 dark:aria-invalid:inset-ring-destructive/50 forced-colors:border [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/80",
        outline:
          "bg-background inset-ring-border hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-input/30 dark:inset-ring-input dark:hover:bg-input/50",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
        ghost:
          "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50 forced-colors:border-0",
        destructive:
          "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/70",
        link: "text-primary underline-offset-4 hover:underline forced-colors:border-0",
        success:
          "bg-success text-success-foreground hover:bg-success/90 focus-visible:ring-success/70 focus-visible:inset-ring-success/40",
        error:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90 focus-visible:ring-destructive/70 focus-visible:inset-ring-destructive/40 motion-safe:animate-button-shake",
      },
      size: {
        default:
          "h-9 gap-1.5 px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        xs: "h-6 gap-1 rounded-[min(var(--radius-md),8px)] px-2 text-xs in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
        sm: "h-8 gap-1 rounded-[min(var(--radius-md),10px)] px-2.5 in-data-[slot=button-group]:rounded-md has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5",
        lg: "h-10 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2",
        icon: "size-9 shrink-0",
        "icon-xs":
          "size-6 shrink-0 rounded-[min(var(--radius-md),8px)] in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-2.5 pointer-coarse:in-data-[slot=button-group]:after:hidden [&_svg:not([class*='size-'])]:size-3",
        "icon-sm":
          "size-8 shrink-0 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-md pointer-coarse:after:absolute pointer-coarse:after:-inset-1.5 pointer-coarse:in-data-[slot=button-group]:after:hidden",
        "icon-lg": "size-10 shrink-0",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

type ButtonVariant = Exclude<
  NonNullable<VariantProps<typeof buttonVariants>["variant"]>,
  "success" | "error"
>

type ButtonClickEvent = Parameters<
  NonNullable<ButtonPrimitive.Props["onClick"]>
>[0]

type ButtonProps = Omit<ButtonPrimitive.Props, "onClick"> &
  Omit<VariantProps<typeof buttonVariants>, "variant"> &
  ButtonFeedbackOptions & {
    variant?: ButtonVariant | null
    onClick?: (event: ButtonClickEvent) => unknown
    feedback?: boolean
    loading?: boolean
    status?: ButtonStatus
    loadingLabel?: React.ReactNode
    successLabel?: React.ReactNode
    errorLabel?: React.ReactNode | ((error: unknown) => React.ReactNode)
  }

const stateTransitionTime = 220
const statuses: ButtonStatus[] = ["idle", "loading", "success", "error"]
const quietVariants: ButtonVariant[] = ["ghost", "link"]
const quietStatusClasses: Partial<Record<ButtonStatus, string>> = {
  success: "text-success hover:text-success",
  error:
    "text-destructive hover:text-destructive motion-safe:animate-button-shake",
}

function isThenable(value: unknown): value is PromiseLike<unknown> {
  return (
    typeof value === "object" &&
    value !== null &&
    typeof (value as PromiseLike<unknown>).then === "function"
  )
}

function useStatusTransition(status: ButtonStatus) {
  const [shown, setShown] = React.useState(status)
  const [leaving, setLeaving] = React.useState<ButtonStatus | null>(null)
  const [transitionKey, setTransitionKey] = React.useState(0)

  if (status !== shown) {
    setLeaving(shown)
    setShown(status)
    setTransitionKey((key) => key + 1)
  }

  React.useEffect(() => {
    if (leaving === null) {
      return
    }
    const timer = setTimeout(() => setLeaving(null), stateTransitionTime)
    return () => clearTimeout(timer)
  }, [leaving, transitionKey])

  return { leaving, transitionKey }
}

function useLayerWidth(status: ButtonStatus, active: boolean) {
  const contentRef = React.useRef<HTMLSpanElement>(null)
  const layersRef = React.useRef(new Map<ButtonStatus, HTMLElement>())
  const observerRef = React.useRef<ResizeObserver | null>(null)
  const statusRef = React.useRef(status)

  const applyWidth = React.useCallback(() => {
    const content = contentRef.current
    const layer = layersRef.current.get(statusRef.current)
    if (!content || !layer) {
      return
    }
    const natural = parseFloat(getComputedStyle(layer).width)
    if (!Number.isFinite(natural)) {
      return
    }
    content.style.width = `${natural}px`
    if (!content.hasAttribute("data-measured")) {
      requestAnimationFrame(() => content.setAttribute("data-measured", ""))
    }
  }, [])

  React.useLayoutEffect(() => {
    statusRef.current = status
    if (active) {
      applyWidth()
    }
  }, [active, applyWidth, status])

  React.useLayoutEffect(() => {
    if (!active || typeof ResizeObserver === "undefined") {
      return
    }
    const observer = new ResizeObserver(applyWidth)
    observerRef.current = observer
    layersRef.current.forEach((layer) => observer.observe(layer))
    document.fonts?.addEventListener?.("loadingdone", applyWidth)
    return () => {
      observer.disconnect()
      observerRef.current = null
      document.fonts?.removeEventListener?.("loadingdone", applyWidth)
    }
  }, [active, applyWidth])

  const layerRef = React.useCallback((node: HTMLElement | null) => {
    if (!node) {
      return
    }
    const layer = node.dataset.layer as ButtonStatus
    layersRef.current.set(layer, node)
    observerRef.current?.observe(node)
    return () => {
      observerRef.current?.unobserve(node)
      if (layersRef.current.get(layer) === node) {
        layersRef.current.delete(layer)
      }
    }
  }, [])

  return { contentRef, layerRef }
}

function composeHandlers<Event>(
  first: ((event: Event) => void) | undefined,
  second: (event: Event) => void
) {
  return (event: Event) => {
    first?.(event)
    second(event)
  }
}

function Button({
  className,
  variant = "default",
  size = "default",
  children,
  onClick,
  disabled,
  focusableWhenDisabled,
  feedback,
  loading,
  status: statusProp,
  onStatusChange,
  onError,
  resetAfter,
  loadingLabel,
  successLabel = "Done",
  errorLabel = "Failed",
  onPointerEnter,
  onPointerLeave,
  onFocus,
  onBlur,
  ...props
}: ButtonProps) {
  const internal = useButtonFeedback({ resetAfter, onStatusChange, onError })
  const status = statusProp ?? (loading ? "loading" : internal.status)
  const requested =
    Boolean(feedback) || loading !== undefined || statusProp !== undefined
  const [enabled, setEnabled] = React.useState(requested)
  if (requested && !enabled) {
    setEnabled(true)
  }

  const { leaving, transitionKey } = useStatusTransition(status)
  const iconOnly = size?.startsWith("icon") ?? false
  const widthStatus =
    status === "loading" && loadingLabel === undefined ? "idle" : status
  const { contentRef, layerRef } = useLayerWidth(
    widthStatus,
    enabled && !iconOnly
  )
  const isLoading = status === "loading"
  const quiet = quietVariants.includes(variant ?? "default")
  const resolvedVariant =
    !quiet && status === "success"
      ? "success"
      : !quiet && status === "error"
        ? "error"
        : variant
  const resolvedErrorLabel =
    typeof errorLabel !== "function"
      ? errorLabel
      : internal.error !== undefined
        ? errorLabel(internal.error)
        : "Failed"

  const handleClick = (event: ButtonClickEvent) => {
    if (feedback && internal.isPending()) {
      return
    }
    const result = onClick?.(event)
    if (feedback && isThenable(result)) {
      internal.track(result)
    }
  }

  const layerContent = (layer: ButtonStatus, animated: boolean) => {
    if (layer === "idle") {
      return children
    }
    if (layer === "loading") {
      return (
        <>
          <Spinner aria-hidden size={null} animated={animated} />
          {iconOnly ? null : loadingLabel}
        </>
      )
    }
    if (layer === "success") {
      return (
        <>
          <IconCircleCheck
            className={cn(
              animated &&
                "motion-safe:[&>path:first-child]:animate-button-draw-circle motion-safe:[&>path:first-child]:[stroke-dasharray:57] motion-safe:[&>path:first-child]:[stroke-dashoffset:57] motion-safe:[&>path:last-child]:animate-button-draw-check motion-safe:[&>path:last-child]:[stroke-dasharray:9] motion-safe:[&>path:last-child]:[stroke-dashoffset:9]"
            )}
          />
          {iconOnly ? null : successLabel}
        </>
      )
    }
    return (
      <>
        <IconAlertCircle />
        {iconOnly ? null : resolvedErrorLabel}
      </>
    )
  }

  const button = (
    <ButtonPrimitive
      data-slot="button"
      data-status={enabled ? status : undefined}
      aria-busy={isLoading || undefined}
      disabled={disabled || isLoading}
      focusableWhenDisabled={focusableWhenDisabled ?? (isLoading || undefined)}
      className={cn(
        buttonVariants({ variant: resolvedVariant, size }),
        quiet && quietStatusClasses[status],
        className
      )}
      onClick={handleClick}
      onPointerEnter={composeHandlers(
        onPointerEnter,
        internal.buttonProps.onPointerEnter
      )}
      onPointerLeave={composeHandlers(
        onPointerLeave,
        internal.buttonProps.onPointerLeave
      )}
      onFocus={composeHandlers(onFocus, internal.buttonProps.onFocus)}
      onBlur={composeHandlers(onBlur, internal.buttonProps.onBlur)}
      {...props}
    >
      {enabled ? (
        <span
          ref={contentRef}
          data-slot="button-content"
          className="relative inline-grid grid-cols-[minmax(0,1fr)] place-items-center gap-[inherit] overflow-x-clip *:col-start-1 *:row-start-1 motion-safe:data-measured:transition-[width] motion-safe:data-measured:duration-220 motion-safe:data-measured:ease-[cubic-bezier(0.2,0,0,1)]"
        >
          {statuses.map((layer) => {
            const active = layer === status
            const exiting = layer === leaving
            const namesButton =
              layer === "idle" && isLoading && loadingLabel === undefined

            return (
              <span
                key={active ? `${layer}-${transitionKey}` : layer}
                ref={layerRef}
                data-slot="button-layer"
                data-layer={layer}
                aria-hidden={active || namesButton ? undefined : true}
                className={cn(
                  "inline-flex items-center justify-center gap-[inherit] backface-hidden",
                  !active &&
                    !exiting &&
                    "pointer-events-none absolute opacity-0",
                  active &&
                    transitionKey > 0 &&
                    "origin-bottom motion-safe:animate-button-state-in motion-reduce:animate-in motion-reduce:animation-duration-150 motion-reduce:fade-in-0",
                  exiting &&
                    "pointer-events-none origin-top opacity-0 motion-safe:animate-button-state-out"
                )}
              >
                {layerContent(layer, active || exiting)}
              </span>
            )
          })}
        </span>
      ) : (
        children
      )}
    </ButtonPrimitive>
  )

  if (!enabled) {
    return button
  }

  const announcement =
    status === "loading"
      ? typeof loadingLabel === "string"
        ? loadingLabel
        : "Loading"
      : status === "success"
        ? typeof successLabel === "string"
          ? successLabel
          : "Done"
        : status === "error"
          ? typeof resolvedErrorLabel === "string"
            ? resolvedErrorLabel
            : "Failed"
          : ""

  return (
    <>
      {button}
      <span data-slot="button-status" role="status" className="sr-only">
        {announcement}
      </span>
    </>
  )
}

export { Button, buttonVariants, useButtonFeedback }
export type { ButtonProps, ButtonStatus, ButtonFeedbackOptions }
```

```ts title="lib/motion.ts"
import * as React from "react"

const easeOut = "cubic-bezier(0.23, 1, 0.32, 1)"
const easeInOut = "cubic-bezier(0.77, 0, 0.175, 1)"
const easeSpring =
  "linear(0, 0.015 2%, 0.0532 4%, 0.1065 6%, 0.1686 8%, 0.2351 10%, 0.3363 13%, 0.4332 16%, 0.5495 20%, 0.648 24%, 0.7287 28%, 0.807 33%, 0.8646 38%, 0.9128 44%, 0.9447 50%, 0.968 57%, 0.9834 65%, 0.9924 74%, 0.9975 85%, 1)"

const duration = {
  press: 100,
  release: 200,
  hover: 150,
  enter: 200,
  exit: 150,
  morph: 300,
} as const

function prefersReducedMotion() {
  return (
    typeof window === "undefined" ||
    typeof window.matchMedia !== "function" ||
    window.matchMedia("(prefers-reduced-motion: reduce)").matches
  )
}

type SizeAxis = "width" | "height"

type SizeMorphOptions = {
  axis: SizeAxis
  enabled?: boolean
  duration?: number
  easing?: string
}

function readSize(element: HTMLElement, axis: SizeAxis) {
  const value = parseFloat(getComputedStyle(element)[axis])
  if (Number.isFinite(value)) {
    return value
  }
  const rect = element.getBoundingClientRect()
  return axis === "width" ? rect.width : rect.height
}

function attachSizeMorph(
  element: HTMLElement,
  axis: SizeAxis,
  time: number,
  easing: string
) {
  if (
    typeof MutationObserver === "undefined" ||
    typeof element.animate !== "function"
  ) {
    return undefined
  }

  let settled = readSize(element, axis)
  let animation: Animation | null = null

  const morph = () => {
    const from = animation ? readSize(element, axis) : settled
    animation?.cancel()
    animation = null
    const to = readSize(element, axis)
    settled = to

    if (
      Math.abs(from - to) < 0.5 ||
      prefersReducedMotion() ||
      !element.isConnected ||
      element.getClientRects().length === 0
    ) {
      element.removeAttribute("data-morphing")
      return
    }

    element.setAttribute("data-morphing", "")
    const running = element.animate(
      [{ [axis]: `${from}px` }, { [axis]: `${to}px` }],
      { duration: time, easing }
    )
    animation = running
    running.onfinish = () => {
      if (animation === running) {
        animation = null
        element.removeAttribute("data-morphing")
        settled = readSize(element, axis)
      }
    }
  }

  const mutations = new MutationObserver(morph)
  mutations.observe(element, {
    childList: true,
    subtree: true,
    characterData: true,
  })

  const resize =
    typeof ResizeObserver === "undefined"
      ? null
      : new ResizeObserver(() => {
          if (!animation) {
            settled = readSize(element, axis)
          }
        })
  resize?.observe(element)

  return () => {
    mutations.disconnect()
    resize?.disconnect()
    animation?.cancel()
    element.removeAttribute("data-morphing")
  }
}

function useSizeMorph<T extends HTMLElement>({
  axis,
  enabled = true,
  duration: time = duration.morph,
  easing = easeOut,
}: SizeMorphOptions): React.RefCallback<T> {
  return React.useCallback(
    (element: T | null) => {
      if (!element || !enabled) {
        return undefined
      }
      return attachSizeMorph(element, axis, time, easing)
    },
    [axis, enabled, time, easing]
  )
}

function useSlidingHighlight(
  barRef: React.RefObject<HTMLElement | null>,
  highlightRef: React.RefObject<HTMLElement | null>,
  selector: string,
  attribute = "data-popup-open"
) {
  React.useLayoutEffect(() => {
    const bar = barRef.current
    const highlight = highlightRef.current
    if (!bar || !highlight) {
      return
    }

    let current: HTMLElement | null = null

    const place = (trigger: HTMLElement, instant: boolean) => {
      if (instant || prefersReducedMotion()) {
        highlight.setAttribute("data-instant", "")
      } else {
        highlight.removeAttribute("data-instant")
      }
      const frame = bar.getBoundingClientRect()
      const box = trigger.getBoundingClientRect()
      const scale = bar.offsetWidth > 0 ? frame.width / bar.offsetWidth : 1
      highlight.style.left = "0px"
      highlight.style.width = `${box.width / scale}px`
      highlight.style.height = `${box.height / scale}px`
      highlight.style.transform = `translate(${(box.left - frame.left) / scale - bar.clientLeft}px, ${(box.top - frame.top) / scale - bar.clientTop}px)`
    }

    const sync = () => {
      const trigger = bar.querySelector<HTMLElement>(selector)
      if (trigger === current) {
        if (trigger) {
          place(trigger, true)
        }
        return
      }
      const appearing = current === null
      current = trigger
      if (!trigger) {
        highlight.removeAttribute("data-visible")
        return
      }
      place(trigger, appearing)
      if (appearing) {
        void highlight.offsetWidth
      }
      highlight.setAttribute("data-visible", "")
    }

    sync()
    const mutations = new MutationObserver(sync)
    mutations.observe(bar, {
      subtree: true,
      childList: true,
      attributes: true,
      attributeFilter: [attribute],
    })
    const resize =
      typeof ResizeObserver === "undefined"
        ? null
        : new ResizeObserver(() => {
            if (current) {
              place(current, true)
            }
          })
    resize?.observe(bar)
    const onScroll = () => {
      if (current) {
        place(current, true)
      }
    }
    bar.addEventListener("scroll", onScroll, { capture: true, passive: true })

    return () => {
      mutations.disconnect()
      resize?.disconnect()
      bar.removeEventListener("scroll", onScroll, { capture: true })
    }
  }, [barRef, highlightRef, selector, attribute])
}

export {
  duration,
  easeInOut,
  easeOut,
  easeSpring,
  prefersReducedMotion,
  useSizeMorph,
  useSlidingHighlight,
}
export type { SizeMorphOptions }
```

Update the import paths to match your project setup.

## Usage

```tsx
import {
  Alert,
  AlertAction,
  AlertClose,
  AlertDescription,
  AlertTitle,
} from "@/components/ui/alert"
```

```tsx
<Alert>
  <IconTerminal2 />
  <AlertTitle>Heads up</AlertTitle>
  <AlertDescription>
    You can add components to your app using the CLI.
  </AlertDescription>
</Alert>
```

## Composition

```text
Alert
├── AlertTitle
├── AlertDescription
├── AlertAction
└── AlertClose
```

## Examples

### Variants

`variant` colors only the icon, so the surface stays neutral and status reads at a glance without shouting.

```tsx title="components/examples/alert/variants.tsx"
import {
  IconAlertTriangle,
  IconCircleCheck,
  IconCircleX,
  IconInfoCircle,
  IconTerminal2,
} from "@tabler/icons-react"

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"

const alerts = [
  {
    variant: "default",
    icon: IconTerminal2,
    title: "Heads up",
    description: "You can add components to your app using the CLI.",
  },
  {
    variant: "destructive",
    icon: IconCircleX,
    title: "Payment failed",
    description: "Your card was declined. Update your billing details.",
  },
  {
    variant: "success",
    icon: IconCircleCheck,
    title: "Changes saved",
    description: "Your profile is now visible to your team.",
  },
  {
    variant: "info",
    icon: IconInfoCircle,
    title: "New version available",
    description: "Reload the page to get the latest features.",
  },
  {
    variant: "warning",
    icon: IconAlertTriangle,
    title: "Trial ends in 3 days",
    description: "Add a payment method to keep your workspace active.",
  },
] as const

export function AlertVariants() {
  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      {alerts.map(({ variant, icon: Icon, title, description }) => (
        <Alert key={variant} variant={variant}>
          <Icon />
          <AlertTitle>{title}</AlertTitle>
          <AlertDescription>{description}</AlertDescription>
        </Alert>
      ))}
    </div>
  )
}
```

### Soft

`appearance="soft"` drops the border and tints the whole surface with the variant color.

```tsx title="components/examples/alert/soft.tsx"
import {
  IconAlertTriangle,
  IconCircleCheck,
  IconCircleX,
  IconInfoCircle,
  IconTerminal2,
} from "@tabler/icons-react"

import {
  Alert,
  AlertAction,
  AlertClose,
  AlertDescription,
  AlertTitle,
} from "@/components/ui/alert"
import { Button } from "@/components/ui/button"

const alerts = [
  {
    variant: "default",
    icon: IconTerminal2,
    title: "Heads up",
    description: "You can add components to your app using the CLI.",
  },
  {
    variant: "destructive",
    icon: IconCircleX,
    title: "Payment failed",
    description: "Your card was declined. Update your billing details.",
  },
  {
    variant: "success",
    icon: IconCircleCheck,
    title: "Changes saved",
    description: "Your profile is now visible to your team.",
  },
  {
    variant: "info",
    icon: IconInfoCircle,
    title: "New version available",
    description: "Reload the page to get the latest features.",
  },
  {
    variant: "warning",
    icon: IconAlertTriangle,
    title: "Trial ends in 3 days",
    description: "Add a payment method to keep your workspace active.",
  },
] as const

export function AlertSoft() {
  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      {alerts.map(({ variant, icon: Icon, title, description }) => (
        <Alert key={variant} variant={variant} appearance="soft">
          <Icon />
          <AlertTitle>{title}</AlertTitle>
          <AlertDescription>{description}</AlertDescription>
        </Alert>
      ))}
      <Alert variant="success" appearance="soft">
        <IconCircleCheck />
        <AlertTitle>Message archived</AlertTitle>
        <AlertAction>
          <Button variant="ghost" size="xs">
            Undo
          </Button>
        </AlertAction>
        <AlertClose />
      </Alert>
    </div>
  )
}
```

### Partial content

Title only, description only and no icon all line up without extra classes. The grid places each part in its own column.

```tsx title="components/examples/alert/partial-content.tsx"
import { IconCircleCheck, IconInfoCircle } from "@tabler/icons-react"

import {
  Alert,
  AlertClose,
  AlertDescription,
  AlertTitle,
} from "@/components/ui/alert"

export function AlertPartialContent() {
  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <Alert variant="success">
        <IconCircleCheck />
        <AlertTitle>Invite sent</AlertTitle>
        <AlertClose />
      </Alert>
      <Alert>
        <IconInfoCircle />
        <AlertDescription>
          Sessions expire after 30 days of inactivity.
        </AlertDescription>
      </Alert>
      <Alert>
        <AlertTitle>No icon</AlertTitle>
        <AlertDescription>The content starts at the padding.</AlertDescription>
      </Alert>
    </div>
  )
}
```

### Actions

Put buttons in `<AlertAction />`. They align with the first line of the title, even when it wraps.

```tsx title="components/examples/alert/actions.tsx"
import { IconInfoCircle, IconTerminal2 } from "@tabler/icons-react"

import {
  Alert,
  AlertAction,
  AlertClose,
  AlertDescription,
  AlertTitle,
} from "@/components/ui/alert"
import { Button } from "@/components/ui/button"

export function AlertActions() {
  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <Alert variant="info">
        <IconInfoCircle />
        <AlertTitle>
          A new version of the dashboard is available with faster charts, saved
          filters and a redesigned sidebar
        </AlertTitle>
        <AlertDescription>
          Reloading keeps your current filters.
        </AlertDescription>
        <AlertAction>
          <Button size="xs">Reload</Button>
        </AlertAction>
      </Alert>
      <Alert>
        <IconTerminal2 />
        <AlertTitle>Message archived</AlertTitle>
        <AlertAction>
          <Button variant="secondary" size="xs">
            Undo
          </Button>
        </AlertAction>
        <AlertClose />
      </Alert>
    </div>
  )
}
```

### Dismissible

Add `<AlertClose />` to let people dismiss it. The alert collapses smoothly and the content below slides up. Dismiss it from the keyboard and focus moves to the next control, here “Reset”.

```tsx title="components/examples/alert/dismissible.tsx"
"use client"

import * as React from "react"
import { IconInfoCircle } from "@tabler/icons-react"

import {
  Alert,
  AlertClose,
  AlertDescription,
  AlertTitle,
} from "@/components/ui/alert"
import { Button } from "@/components/ui/button"

export function AlertDismissible() {
  const [key, setKey] = React.useState(0)

  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <Alert key={key} variant="info">
        <IconInfoCircle />
        <AlertTitle>We’ve updated our terms</AlertTitle>
        <AlertDescription>
          Read the <a href="#">new terms</a> before your next billing date.
        </AlertDescription>
        <AlertClose />
      </Alert>
      <div className="flex gap-2">
        <Button variant="outline" size="sm" onClick={() => setKey(key + 1)}>
          Reset
        </Button>
        <Button variant="outline" size="sm">
          Next focusable
        </Button>
      </div>
    </div>
  )
}
```

### Controlled

Pass `open` and `onOpenChange` to drive it from your own state. It animates both in and out.

```tsx title="components/examples/alert/controlled.tsx"
"use client"

import * as React from "react"
import { IconCircleCheck } from "@tabler/icons-react"

import {
  Alert,
  AlertClose,
  AlertDescription,
  AlertTitle,
} from "@/components/ui/alert"
import { Button } from "@/components/ui/button"

export function AlertControlled() {
  const [open, setOpen] = React.useState(true)

  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <Button
        variant="outline"
        size="sm"
        className="self-start"
        onClick={() => setOpen(!open)}
      >
        {open ? "Hide" : "Show"} alert
      </Button>
      <Alert variant="success" open={open} onOpenChange={setOpen}>
        <IconCircleCheck />
        <AlertTitle>Deployment complete</AlertTitle>
        <AlertDescription>
          Your changes are live on production.
        </AlertDescription>
        <AlertClose />
      </Alert>
      <p className="text-sm text-muted-foreground">Content below slides up.</p>
    </div>
  )
}
```

### Cancel a dismiss

Call `event.preventDefault()` in the close button’s `onClick` to keep the alert open, for example to ask for confirmation first.

```tsx title="components/examples/alert/cancel-dismiss.tsx"
"use client"

import * as React from "react"
import { IconAlertTriangle } from "@tabler/icons-react"

import {
  Alert,
  AlertClose,
  AlertDescription,
  AlertTitle,
} from "@/components/ui/alert"

export function AlertCancelDismiss() {
  const [confirmed, setConfirmed] = React.useState(false)

  return (
    <Alert variant="warning" className="max-w-lg">
      <IconAlertTriangle />
      <AlertTitle>Unsaved changes</AlertTitle>
      <AlertDescription>
        {confirmed
          ? "Click dismiss again to close."
          : "The first click is cancelled with event.preventDefault()."}
      </AlertDescription>
      <AlertClose
        onClick={(event) => {
          if (!confirmed) {
            event.preventDefault()
            setConfirmed(true)
          }
        }}
      />
    </Alert>
  )
}
```

### Inserted after load

Alerts that mount after the page has loaded fade and slide in, and screen readers announce them. Alerts already in the server HTML appear without motion.

```tsx title="components/examples/alert/inserted.tsx"
"use client"

import * as React from "react"
import { IconCircleX } from "@tabler/icons-react"

import {
  Alert,
  AlertClose,
  AlertDescription,
  AlertTitle,
} from "@/components/ui/alert"
import { Button } from "@/components/ui/button"

export function AlertInserted() {
  const [errors, setErrors] = React.useState<number[]>([])

  return (
    <div className="flex w-full max-w-lg flex-col gap-3">
      <Button
        variant="outline"
        size="sm"
        className="self-start"
        onClick={() => setErrors([...errors, Date.now()])}
      >
        Submit form
      </Button>
      {errors.map((id) => (
        <Alert
          key={id}
          variant="destructive"
          onOpenChange={() =>
            setErrors((current) => current.filter((error) => error !== id))
          }
        >
          <IconCircleX />
          <AlertTitle>Email is already in use</AlertTitle>
          <AlertDescription>Try signing in instead.</AlertDescription>
          <AlertClose />
        </Alert>
      ))}
    </div>
  )
}
```

### Rich content

Inline icons and links inside the description keep their own style. Use `render` to give the title a heading level.

```tsx title="components/examples/alert/rich-content.tsx"
import {
  IconExternalLink,
  IconInfoCircle,
  IconTerminal2,
} from "@tabler/icons-react"

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"

export function AlertRichContent() {
  return (
    <Alert className="max-w-lg">
      <IconInfoCircle />
      <AlertTitle render={<h3 />}>Rendered as an h3</AlertTitle>
      <AlertDescription>
        <p>
          Press <IconTerminal2 className="inline size-4 align-text-bottom" /> to
          open the terminal, or read the <a href="#">docs</a>.
        </p>
        <Button
          variant="outline"
          size="sm"
          nativeButton={false}
          render={<a href="#" />}
        >
          Open guide <IconExternalLink />
        </Button>
      </AlertDescription>
    </Alert>
  )
}
```

### Right to left

The icon, content and close button mirror with the reading direction.

```tsx title="components/examples/alert/rtl.tsx"
import { IconAlertTriangle } from "@tabler/icons-react"

import {
  Alert,
  AlertClose,
  AlertDescription,
  AlertTitle,
} from "@/components/ui/alert"

export function AlertRtl() {
  return (
    <div dir="rtl" className="w-full max-w-lg">
      <Alert variant="warning">
        <IconAlertTriangle />
        <AlertTitle>تنتهي الفترة التجريبية خلال 3 أيام</AlertTitle>
        <AlertDescription>
          أضف طريقة دفع لإبقاء مساحة العمل نشطة.
        </AlertDescription>
        <AlertClose />
      </Alert>
    </div>
  )
}
```

## Keyboard

| Key | Action |
| --- | --- |
| `Tab` | Moves focus to links, actions and the close button inside the alert. |
| `Enter` `Space` | Activates the focused close button. Once the alert has collapsed, focus moves to the next focusable element, or the previous one when nothing follows. |

## Accessibility

- `destructive` and `warning` alerts get `role="alert"` and are announced right away. Every other variant gets `role="status"` and waits for a pause.
- The close button is labelled “Dismiss”. Pass `aria-label` to translate it.
- Focus is never lost when a focused alert closes.
- With reduced motion on, alerts appear and disappear instantly.

## API reference

Every part accepts the attributes of the element it renders. The variants are exported as `alertVariants` for styling other elements the same way.

### Alert

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `"default" \| "destructive" \| "success" \| "info" \| "warning"` | `"default"` | Colors the icon. destructive and warning are announced assertively. |
| `appearance` | `"outline" \| "soft"` | `"outline"` |  |
| `open` | `boolean` | – |  |
| `defaultOpen` | `boolean` | `true` |  |
| `onOpenChange` | `(open: boolean) => void` | – | Called when the close button is pressed. |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="alert"` | Target the alert in CSS. |
| `data-variant` | The current variant. |
| `data-appearance` | The current appearance. |
| `data-ending-style` | Present while the alert collapses after a dismiss. |

### AlertTitle

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="alert-title"` | Target titles in CSS. |

### AlertDescription

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="alert-description"` | Target descriptions in CSS. Plain links inside are underlined for you. |

### AlertAction

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `render` | `ReactElement \| (props, state) => ReactElement` | `<div>` |  |

| Attribute | Description |
| --- | --- |
| `data-slot="alert-action"` | Target the action area in CSS. |

### AlertClose

A ghost `<Button />` that closes the nearest alert. It accepts every Button prop and must be used inside `<Alert />`.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `aria-label` | `string` | `"Dismiss"` |  |
| `children` | `ReactNode` | `<IconX />` |  |
| `onClick` | `(event: MouseEvent) => void` | – | Call event.preventDefault() to keep the alert open. |

| Attribute | Description |
| --- | --- |
| `data-slot="alert-close"` | Target the close button in CSS. |

## Notes for AI assistants

- Install a component with the shadcn CLI: `npx shadcn@latest add https://hextaui.com/r/<name>.json`. It adds the source, the HextaUI theme tokens and any HextaUI components it depends on. `https://hextaui.com/r/all.json` installs every component.
- The code is then owned by the project, like shadcn/ui. There is no HextaUI npm package. HextaUI is MIT licensed and free for personal and commercial use.
- Behavior and accessibility come from Base UI (`@base-ui/react`). Compose with the `render` prop, not `asChild`.
- Styling uses Tailwind CSS v4 with theme tokens. Merge classes with `cn` from the `cn` package.
- Icons come from `@tabler/icons-react`.
- Import components from `@/components/ui/<name>`, hooks from `@/hooks/<name>` and utilities from `@/lib/<name>`.

Every HextaUI doc: https://hextaui.com/llms.txt
