shadimage
cubby-ui
uialert-dialogsource: ok · hit

Alert-dialog

A alert-dialog component.

npx shadcn add "https://www.cubby-ui.dev/r/alert-dialog.json"
free

Interaction

modal

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
@hugeicons/react@hugeicons/core-free-icons

Registry dependencies

@cubby-ui/button@cubby-ui/scroll-area@cubby-ui/elevated

Files1 · source cached 10/12/2026, 6:21:59 AM

registry/default/alert-dialog/alert-dialog.tsx
"use client";

import * as React from "react";
import { AlertDialog as BaseAlertDialog } from "@base-ui/react/alert-dialog";
import { cn } from "@/lib/utils";
import { Button } from "@/registry/default/button/button";
import {
  elevatedSurface,
  type SurfaceLevel,
} from "@/registry/default/lib/elevated";
import {
  ScrollArea,
  type ScrollAreaProps,
} from "@/registry/default/scroll-area/scroll-area";

import { HugeiconsIcon } from "@hugeicons/react";
import { Cancel01Icon } from "@hugeicons/core-free-icons";
const AlertDialog = BaseAlertDialog.Root;

const createAlertDialogHandle = BaseAlertDialog.createHandle;

function AlertDialogPortal({ ...props }: BaseAlertDialog.Portal.Props) {
  return <BaseAlertDialog.Portal data-slot="alert-dialog-portal" {...props} />;
}

function AlertDialogTrigger({ ...props }: BaseAlertDialog.Trigger.Props) {
  return (
    <BaseAlertDialog.Trigger data-slot="alert-dialog-trigger" {...props} />
  );
}

function AlertDialogClose({ ...props }: BaseAlertDialog.Close.Props) {
  return <BaseAlertDialog.Close data-slot="alert-dialog-close" {...props} />;
}

function AlertDialogBackdrop({
  className,
  ...props
}: BaseAlertDialog.Backdrop.Props) {
  return (
    <BaseAlertDialog.Backdrop
      className={cn(
        "ease-out-expo fixed inset-0 min-h-dvh bg-black/40 transition-all duration-200 supports-[-webkit-touch-callout:none]:absolute",
        "backdrop-blur-sm data-ending-style:opacity-0 data-starting-style:opacity-0",
        className,
      )}
      {...props}
    />
  );
}

function AlertDialogViewport({
  className,
  ...props
}: BaseAlertDialog.Viewport.Props) {
  return (
    <BaseAlertDialog.Viewport
      data-slot="alert-dialog-viewport"
      className={cn(
        "fixed inset-0 flex items-center justify-center overflow-hidden px-4 py-6",
        className,
      )}
      {...props}
    />
  );
}

function AlertDialogContent({
  className,
  children,
  showCloseButton = false,
  variant = "default",
  level = 5,
  shadowLevel = 5,
  ...props
}: BaseAlertDialog.Popup.Props & {
  showCloseButton?: boolean;
  variant?: "default" | "inset";
  /** Surface elevation level for the dialog bg (1-8). Defaults to 5. Bump to 7 for a critical/destructive confirmation that needs extra gravity. */
  level?: SurfaceLevel;
  /** Shadow weight (1-8). Pinned to 5 by default. */
  shadowLevel?: SurfaceLevel;
}) {
  return (
    <AlertDialogPortal>
      <AlertDialogBackdrop />
      <AlertDialogViewport>
        <BaseAlertDialog.Popup
          data-slot="alert-dialog-content"
          data-variant={variant}
          data-level={level}
          className={cn(
            "text-popover-foreground relative z-50 flex max-h-full min-h-0 w-full max-w-full min-w-0 flex-col overflow-hidden",
            "rounded-2xl sm:max-w-lg",
            // Surface elevation — bg + shadow + rim overlay (rim uses ::after at z-[2])
            elevatedSurface(level, shadowLevel),
            // Nested dialog offset
            "-translate-y-[calc(1.25rem*var(--nested-dialogs))]",
            // Scale effect for nested dialogs
            "scale-[calc(1-0.1*var(--nested-dialogs))]",
            // Animation duration
            "ease-out-expo transition-all duration-200",
            // Animations: scale and fade
            "data-starting-style:translate-y-[calc(1.25rem)] data-starting-style:scale-95 data-starting-style:opacity-0",
            "data-ending-style:translate-y-[calc(1.25rem)] data-ending-style:scale-95 data-ending-style:opacity-0",
            // Nested dialog overlay — uses ::before (not ::after, that's the rim) at z-3 so it paints above content AND above the rim
            "before:pointer-events-none before:absolute before:inset-0 before:z-3 before:hidden before:rounded-[inherit] before:bg-black/5 before:opacity-0 before:transition-[opacity,display] before:transition-discrete before:duration-200",
            "data-nested-dialog-open:before:block data-nested-dialog-open:before:opacity-100",
            "starting:data-nested-dialog-open:before:opacity-0",
            className,
          )}
          {...props}
        >
          {children}
          {showCloseButton && (
            <AlertDialogClose
              aria-label="Close"
              className="absolute end-2 top-2"
              render={<Button size="icon_sm" variant="ghost" />}
            >
              <HugeiconsIcon icon={Cancel01Icon} strokeWidth={2} />
            </AlertDialogClose>
          )}
        </BaseAlertDialog.Popup>
      </AlertDialogViewport>
    </AlertDialogPortal>
  );
}

// Slot-presence padding uses ancestor `:has()` on `[data-slot=alert-dialog-content]`
// so header/body/footer can be wrapped in forms, ErrorBoundaries, or fragments
// without breaking spacing — adjacent-sibling selectors wouldn't reach through.
function AlertDialogHeader({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      data-slot="alert-dialog-header"
      className={cn(
        "flex flex-col gap-2 px-6 pt-6 pb-3",
        // Header alone with footer (no body anywhere in content)
        "in-[[data-slot=alert-dialog-content]:not(:has([data-slot=alert-dialog-body])):has([data-slot=alert-dialog-footer])]:pb-6",
        // Header alone (no body, no footer)
        "in-[[data-slot=alert-dialog-content]:not(:has([data-slot=alert-dialog-body])):not(:has([data-slot=alert-dialog-footer]))]:pb-6",
        className,
      )}
      {...props}
    />
  );
}

function AlertDialogBody({
  className,
  nativeScroll = false,
  fadeEdges = true,
  scrollbarGutter = false,
  persistScrollbar,
  hideScrollbar,
  children,
  ...props
}: React.HTMLAttributes<HTMLDivElement> & {
  nativeScroll?: boolean;
} & Pick<
    ScrollAreaProps,
    "fadeEdges" | "scrollbarGutter" | "persistScrollbar" | "hideScrollbar"
  >) {
  return (
    <div
      data-slot="alert-dialog-body"
      className={cn(
        "flex min-h-0 flex-1 flex-col overflow-hidden",
        // No header anywhere in content → body needs its own top padding
        "in-[[data-slot=alert-dialog-content]:not(:has([data-slot=alert-dialog-header]))]:pt-5",
        // No footer anywhere in content → body needs its own bottom padding
        "in-[[data-slot=alert-dialog-content]:not(:has([data-slot=alert-dialog-footer]))]:pb-5",
        // Inset variant: still need bottom padding when a footer follows so the
        // body's content doesn't crowd the footer's top border
        "in-data-[variant=inset]:in-[[data-slot=alert-dialog-content]:has([data-slot=alert-dialog-footer])]:pb-5",
      )}
    >
      <ScrollArea
        className="flex-1"
        fadeEdges={fadeEdges}
        scrollbarGutter={scrollbarGutter}
        persistScrollbar={persistScrollbar}
        hideScrollbar={hideScrollbar}
        nativeScroll={nativeScroll}
      >
        <div className={cn("px-6 py-1", className)} {...props}>
          {children}
        </div>
      </ScrollArea>
    </div>
  );
}

function AlertDialogFooter({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      data-slot="alert-dialog-footer"
      className={cn(
        "flex flex-col-reverse gap-2 px-6 pt-4 pb-6 sm:flex-row sm:justify-end",
        // No header AND no body → footer alone, needs its own top padding
        "in-[[data-slot=alert-dialog-content]:not(:has([data-slot=alert-dialog-header])):not(:has([data-slot=alert-dialog-body]))]:pt-6",
        // Reduce top padding when body is present
        "not-in-data-[variant=inset]:in-[[data-slot=alert-dialog-content]:has([data-slot=alert-dialog-body])]:pt-3",
        // Inset variant: muted background with top border for separation
        "in-data-[variant=inset]:border-border in-data-[variant=inset]:bg-muted in-data-[variant=inset]:rounded-b-2xl in-data-[variant=inset]:border-t in-data-[variant=inset]:pt-4 in-data-[variant=inset]:pb-4",
        className,
      )}
      {...props}
    />
  );
}

function AlertDialogTitle({
  className,
  ...props
}: BaseAlertDialog.Title.Props) {
  return (
    <BaseAlertDialog.Title
      className={cn(
        "text-foreground text-lg leading-none font-semibold tracking-tight text-balance",
        className,
      )}
      {...props}
    />
  );
}

function AlertDialogDescription({
  className,
  ...props
}: BaseAlertDialog.Description.Props) {
  return (
    <BaseAlertDialog.Description
      className={cn("text-muted-foreground text-sm text-pretty", className)}
      {...props}
    />
  );
}

export {
  AlertDialog,
  createAlertDialogHandle,
  AlertDialogPortal,
  AlertDialogBackdrop,
  AlertDialogViewport,
  AlertDialogContent,
  AlertDialogTrigger,
  AlertDialogClose,
  AlertDialogHeader,
  AlertDialogBody,
  AlertDialogFooter,
  AlertDialogTitle,
  AlertDialogDescription,
};

More from cubby-ui

Browse all components in this library →