shadimage
Iconiq
uib-alert-dialogsource: ok · refreshed

Alert Dialog (Base UI)

Alert dialog with asChild triggers, action variants, optional media, controlled async flows, and reduced-motion aware transitions layered over Base UI primitives.

npx shadcn add "https://iconiqui.com/r/b-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 · medium
ui-primitives@base-ui/react@radix-ui/react-slot
animationmotion

Files1 · source cached 10/2/2026, 1:58:30 AM

b-alert-dialog.tsx
"use client";

import { AlertDialog as AlertDialogPrimitive } from "@base-ui/react/alert-dialog";
import { Slot } from "@radix-ui/react-slot";
import {
  MotionConfig,
  motion,
  type Transition,
  useReducedMotion,
  type Variants,
} from "motion/react";
import * as React from "react";

import { cn } from "@/lib/utils";

const controlCornerClassName =
  "rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[11px]";

const surfaceCornerClassName =
  "rounded-lg supports-[corner-shape:squircle]:corner-squircle supports-[corner-shape:squircle]:rounded-[12px]";

const alertDialogThemeClassName =
  "[--adlg-surface:#ffffff] [--adlg-foreground:#111111] [--adlg-border:#e3e7ec] [--adlg-ring:rgba(17,17,17,0.16)] [--adlg-muted-foreground:#6d7480] [--adlg-muted:#f5f7fa] [--adlg-action-surface:#111111] [--adlg-action-foreground:#ffffff] [--adlg-destructive:#dc2626] [--adlg-destructive-foreground:#ffffff] [--color-accent:var(--adlg-muted)] [--color-accent-foreground:var(--adlg-foreground)] dark:[--adlg-surface:#0a0a0a] dark:[--adlg-foreground:#f6f3ec] dark:[--adlg-border:#2b2a25] dark:[--adlg-ring:rgba(246,243,236,0.18)] dark:[--adlg-muted-foreground:#9a958a] dark:[--adlg-muted:#1a1a18] dark:[--adlg-action-surface:#f6f3ec] dark:[--adlg-action-foreground:#111111] dark:[--adlg-destructive:#f87171] dark:[--adlg-destructive-foreground:#111111]";

const alertDialogContentClassName = cn(
  surfaceCornerClassName,
  "relative flex w-[min(100%,34rem)] max-w-xl transform-gpu flex-col gap-5 border border-[color:color-mix(in_oklch,var(--adlg-border),transparent_25%)] bg-[color:color-mix(in_oklch,var(--adlg-surface),transparent_4%)] p-6 text-[color:var(--adlg-foreground)] shadow-[0_32px_120px_rgba(15,23,42,0.18)] outline-none supports-[backdrop-filter]:bg-[color:color-mix(in_oklch,var(--adlg-surface),transparent_8%)] sm:p-7"
);

type AlertDialogContextValue = {
  actionsRef: React.RefObject<AlertDialogPrimitive.Root.Actions | null>;
  open: boolean;
};

const AlertDialogContext = React.createContext<AlertDialogContextValue | null>(
  null
);

const FLUID_EASE: [number, number, number, number] = [0.22, 1, 0.36, 1];
const FLUID_EXIT_EASE: [number, number, number, number] = [0.4, 0, 0.2, 1];

const overlayEnterTransition: Transition = {
  opacity: { duration: 0.44, ease: FLUID_EASE },
  backdropFilter: { duration: 0.52, ease: FLUID_EASE },
};

const overlayExitTransition: Transition = {
  opacity: { duration: 0.32, ease: FLUID_EXIT_EASE },
  backdropFilter: { duration: 0.28, ease: FLUID_EXIT_EASE },
};

const overlayVariants: Variants = {
  hidden: {
    opacity: 0,
    backdropFilter: "blur(0px)",
    transition: overlayExitTransition,
  },
  visible: {
    opacity: 1,
    backdropFilter: "blur(10px)",
    transition: overlayEnterTransition,
  },
};

const reducedOverlayVariants: Variants = {
  hidden: { opacity: 0, transition: { duration: 0.1, ease: FLUID_EXIT_EASE } },
  visible: { opacity: 1, transition: { duration: 0.12, ease: FLUID_EASE } },
};

const alertDialogTriggerClassName = cn(
  controlCornerClassName,
  "inline-flex min-h-11 items-center justify-center bg-[color:var(--adlg-action-surface)] px-4 py-2.5 font-medium text-[14px] text-[color:var(--adlg-action-foreground)] tracking-[-0.01em] transition-[transform,background-color] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] hover:-translate-y-px hover:bg-[color:color-mix(in_oklch,var(--adlg-action-surface),transparent_10%)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:color-mix(in_oklch,var(--adlg-ring),transparent_50%)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--adlg-surface)] active:translate-y-0 active:bg-[color:color-mix(in_oklch,var(--adlg-action-surface),transparent_20%)] disabled:pointer-events-none disabled:opacity-50"
);

const alertDialogTriggerSmClassName = cn(
  controlCornerClassName,
  "inline-flex h-8 min-h-8 translate-y-px items-center bg-[color:var(--adlg-action-surface)] px-3 py-0 font-medium text-[13px] text-[color:var(--adlg-action-foreground)] tracking-[-0.01em] transition-[transform,background-color] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] hover:-translate-y-px hover:bg-[color:color-mix(in_oklch,var(--adlg-action-surface),transparent_10%)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:color-mix(in_oklch,var(--adlg-ring),transparent_50%)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--adlg-surface)] active:translate-y-0 active:bg-[color:color-mix(in_oklch,var(--adlg-action-surface),transparent_20%)] disabled:pointer-events-none disabled:opacity-50"
);

const alertDialogActionClassName = cn(
  controlCornerClassName,
  "inline-flex min-h-11 items-center justify-center bg-[color:var(--adlg-destructive)] px-4 py-2.5 font-medium text-[14px] text-[color:var(--adlg-destructive-foreground)] tracking-[-0.01em] transition-[transform,filter] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] hover:-translate-y-px hover:brightness-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:color-mix(in_oklch,var(--adlg-destructive),transparent_30%)] focus-visible:ring-offset-2 active:translate-y-0 active:brightness-90 disabled:pointer-events-none disabled:opacity-50"
);

const alertDialogDefaultActionClassName = cn(
  controlCornerClassName,
  "inline-flex min-h-11 items-center justify-center bg-[color:var(--adlg-action-surface)] px-4 py-2.5 font-medium text-[14px] text-[color:var(--adlg-action-foreground)] tracking-[-0.01em] transition-[transform,background-color] duration-200 ease-[cubic-bezier(0.16,1,0.3,1)] hover:-translate-y-px hover:bg-[color:color-mix(in_oklch,var(--adlg-action-surface),transparent_10%)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:color-mix(in_oklch,var(--adlg-ring),transparent_50%)] focus-visible:ring-offset-2 active:translate-y-0 active:bg-[color:color-mix(in_oklch,var(--adlg-action-surface),transparent_20%)] disabled:pointer-events-none disabled:opacity-50"
);

const alertDialogCancelClassName = cn(
  controlCornerClassName,
  "inline-flex min-h-11 items-center justify-center bg-[color:color-mix(in_oklch,var(--adlg-muted),transparent_45%)] px-4 py-2.5 font-medium text-[14px] text-[color:var(--adlg-muted-foreground)] tracking-[-0.01em] transition-colors duration-150 hover:bg-accent/60 hover:text-[color:var(--adlg-foreground)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:color-mix(in_oklch,var(--adlg-ring),transparent_50%)] focus-visible:ring-offset-2 focus-visible:ring-offset-[color:var(--adlg-surface)] disabled:pointer-events-none disabled:opacity-50"
);

type PopupRenderProps = React.HTMLAttributes<HTMLDivElement> & {
  children?: React.ReactNode;
  className?: string;
  ref?: React.Ref<HTMLDivElement>;
  style?: React.CSSProperties;
};

type BackdropRenderProps = React.HTMLAttributes<HTMLDivElement> & {
  className?: string;
  ref?: React.Ref<HTMLDivElement>;
  style?: React.CSSProperties;
};

function setRef<T>(ref: React.Ref<T> | undefined, value: T | null) {
  if (typeof ref === "function") {
    ref(value);
    return;
  }

  if (ref) {
    (ref as React.MutableRefObject<T | null>).current = value;
  }
}

function resolvePopupProps(popupProps: PopupRenderProps) {
  const {
    children: popupChildren,
    className: popupClassName,
    onAnimationEnd: _onAnimationEnd,
    onAnimationIteration: _onAnimationIteration,
    onAnimationStart: _onAnimationStart,
    onDrag: _onDrag,
    onDragEnd: _onDragEnd,
    onDragEnter: _onDragEnter,
    onDragExit: _onDragExit,
    onDragLeave: _onDragLeave,
    onDragOver: _onDragOver,
    onDragStart: _onDragStart,
    onDrop: _onDrop,
    ref: popupRef,
    style: popupStyle,
    ...resolvedPopupProps
  } = popupProps;

  return {
    popupChildren,
    popupClassName,
    popupRef,
    popupStyle,
    resolvedPopupProps,
  };
}

function resolveBackdropProps(backdropProps: BackdropRenderProps) {
  const {
    className: backdropClassName,
    onAnimationEnd: _onAnimationEnd,
    onAnimationIteration: _onAnimationIteration,
    onAnimationStart: _onAnimationStart,
    onDrag: _onDrag,
    onDragEnd: _onDragEnd,
    onDragStart: _onDragStart,
    ref: backdropRef,
    style: backdropStyle,
    ...resolvedBackdropProps
  } = backdropProps;

  return {
    backdropClassName,
    backdropRef,
    backdropStyle,
    resolvedBackdropProps,
  };
}

const contentVariants: Variants = {
  hidden: {
    opacity: 0,
    scaleX: 0.94,
    scaleY: 0.82,
    y: 24,
    filter: "blur(14px)",
  },
  visible: {
    opacity: 1,
    scaleX: 1,
    scaleY: 1,
    y: 0,
    filter: "blur(0px)",
    transition: {
      opacity: { duration: 0.46, ease: FLUID_EASE },
      scaleX: { duration: 0.54, ease: FLUID_EASE },
      scaleY: { duration: 0.58, ease: FLUID_EASE },
      y: { duration: 0.52, ease: FLUID_EASE },
      filter: { duration: 0.56, ease: FLUID_EASE },
      staggerChildren: 0.065,
      delayChildren: 0.1,
    },
  },
  exit: {
    opacity: 0,
    scaleX: 0.98,
    scaleY: 0.92,
    y: 12,
    filter: "blur(6px)",
    transition: {
      opacity: { duration: 0.28, ease: FLUID_EXIT_EASE },
      scaleX: { duration: 0.3, ease: FLUID_EXIT_EASE },
      scaleY: { duration: 0.32, ease: FLUID_EXIT_EASE },
      y: { duration: 0.3, ease: FLUID_EXIT_EASE },
      filter: { duration: 0.26, ease: FLUID_EXIT_EASE },
    },
  },
};

const reducedContentVariants: Variants = {
  hidden: { opacity: 0 },
  visible: {
    opacity: 1,
    transition: { duration: 0.12, ease: FLUID_EASE },
  },
  exit: {
    opacity: 0,
    transition: { duration: 0.1, ease: FLUID_EXIT_EASE },
  },
};

const childVariants: Variants = {
  hidden: { opacity: 0, y: 14, scale: 0.96, filter: "blur(4px)" },
  visible: {
    opacity: 1,
    y: 0,
    scale: 1,
    filter: "blur(0px)",
    transition: {
      opacity: { duration: 0.4, ease: FLUID_EASE },
      y: { duration: 0.44, ease: FLUID_EASE },
      scale: { duration: 0.42, ease: FLUID_EASE },
      filter: { duration: 0.46, ease: FLUID_EASE },
    },
  },
  exit: {
    opacity: 0,
    y: -6,
    scale: 0.98,
    filter: "blur(2px)",
    transition: { duration: 0.22, ease: FLUID_EXIT_EASE },
  },
};

const reducedChildVariants: Variants = {
  hidden: { opacity: 0 },
  visible: { opacity: 1, transition: { duration: 0.1 } },
  exit: { opacity: 0, transition: { duration: 0.08 } },
};

function mapStaggeredChildren(children: React.ReactNode, variants: Variants) {
  return React.Children.map(children, (child, index) => {
    if (child == null) {
      return null;
    }

    const key =
      React.isValidElement(child) && child.key != null ? child.key : index;

    return (
      <motion.div key={key} variants={variants}>
        {child}
      </motion.div>
    );
  });
}

function useAlertDialogContext() {
  const context = React.useContext(AlertDialogContext);

  if (!context) {
    throw new Error("AlertDialog components must be used within AlertDialog.");
  }

  return context;
}

export interface AlertDialogProps
  extends Omit<
    React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Root>,
    "actionsRef" | "children" | "defaultOpen" | "onOpenChange" | "open"
  > {
  children: React.ReactNode;
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  open?: boolean;
}

function AlertDialog({
  children,
  defaultOpen = false,
  onOpenChange,
  open: openProp,
  ...props
}: AlertDialogProps) {
  const actionsRef = React.useRef<AlertDialogPrimitive.Root.Actions | null>(
    null
  );
  const isControlled = openProp !== undefined;
  const [uncontrolledOpen, setUncontrolledOpen] = React.useState(defaultOpen);
  const open = isControlled ? openProp : uncontrolledOpen;

  const handleOpenChange = React.useCallback(
    (
      nextOpen: boolean,
      eventDetails: AlertDialogPrimitive.Root.ChangeEventDetails
    ) => {
      if (!nextOpen) {
        eventDetails.preventUnmountOnClose();
      }

      onOpenChange?.(nextOpen);

      if (!(eventDetails.isCanceled || isControlled)) {
        setUncontrolledOpen(nextOpen);
      }
    },
    [isControlled, onOpenChange]
  );

  return (
    <AlertDialogContext.Provider value={{ actionsRef, open }}>
      <AlertDialogPrimitive.Root
        {...props}
        actionsRef={actionsRef}
        defaultOpen={isControlled ? undefined : defaultOpen}
        onOpenChange={handleOpenChange}
        open={open}
      >
        {children}
      </AlertDialogPrimitive.Root>
    </AlertDialogContext.Provider>
  );
}

type AlertDialogButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
  asChild?: boolean;
};

type AlertDialogTriggerRef = React.ComponentProps<
  typeof AlertDialogPrimitive.Trigger
>["ref"];

const AlertDialogTrigger = React.forwardRef<
  HTMLElement,
  AlertDialogButtonProps
>(({ asChild, children, className, type = "button", ...props }, ref) => {
  return (
    <AlertDialogPrimitive.Trigger
      {...props}
      className={
        asChild
          ? cn(alertDialogThemeClassName, className)
          : cn(
              alertDialogThemeClassName,
              className ?? alertDialogTriggerClassName
            )
      }
      ref={ref as AlertDialogTriggerRef}
      render={asChild ? <Slot /> : undefined}
      type={asChild ? undefined : type}
    >
      {children}
    </AlertDialogPrimitive.Trigger>
  );
});
AlertDialogTrigger.displayName = "AlertDialogTrigger";

const AlertDialogPortal = AlertDialogPrimitive.Portal;

export interface AlertDialogContentProps
  extends Omit<
    React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Popup>,
    "children" | "className" | "render"
  > {
  children: React.ReactNode;
  className?: string;
  open?: boolean;
}

const AlertDialogContent = React.forwardRef<
  HTMLDivElement,
  AlertDialogContentProps
>(({ children, className, open: openProp, ...props }, ref) => {
  const context = useAlertDialogContext();
  const open = openProp ?? context.open;
  const reduceMotion = useReducedMotion() === true;
  const contentVariantConfig = reduceMotion
    ? reducedContentVariants
    : contentVariants;
  const childVariantConfig = reduceMotion
    ? reducedChildVariants
    : childVariants;
  const overlayVariantConfig = reduceMotion
    ? reducedOverlayVariants
    : overlayVariants;

  return (
    <AlertDialogPortal>
      <MotionConfig reducedMotion={reduceMotion ? "always" : "never"}>
        <AlertDialogPrimitive.Backdrop
          render={(backdropProps) => {
            const {
              backdropClassName,
              backdropRef,
              backdropStyle,
              resolvedBackdropProps,
            } = resolveBackdropProps(backdropProps);

            return (
              <motion.div
                {...resolvedBackdropProps}
                animate={open ? "visible" : "hidden"}
                className={cn(
                  backdropClassName,
                  "fixed inset-0 z-50 bg-black/52"
                )}
                initial="hidden"
                ref={(node) => {
                  setRef(backdropRef, node);
                }}
                style={backdropStyle}
                variants={overlayVariantConfig}
              />
            );
          }}
        />
        <AlertDialogPrimitive.Viewport className="pointer-events-none fixed inset-0 z-[51] grid place-items-center overflow-y-auto p-4">
          <AlertDialogPrimitive.Popup
            {...props}
            className={className}
            render={(popupProps) => {
              const {
                popupChildren,
                popupClassName,
                popupRef,
                popupStyle,
                resolvedPopupProps,
              } = resolvePopupProps(popupProps);

              return (
                <motion.div
                  {...resolvedPopupProps}
                  animate={open ? "visible" : "exit"}
                  className={cn(
                    alertDialogThemeClassName,
                    alertDialogContentClassName,
                    "pointer-events-auto",
                    popupClassName
                  )}
                  initial="hidden"
                  onAnimationComplete={(definition) => {
                    if (definition === "exit" && !open) {
                      context.actionsRef.current?.unmount();
                    }
                  }}
                  ref={(node) => {
                    setRef(popupRef, node);
                    setRef(ref, node);
                  }}
                  style={{
                    ...popupStyle,
                    transformOrigin: "center center",
                  }}
                  variants={contentVariantConfig}
                >
                  {popupChildren}
                </motion.div>
              );
            }}
          >
            {mapStaggeredChildren(children, childVariantConfig)}
          </AlertDialogPrimitive.Popup>
        </AlertDialogPrimitive.Viewport>
      </MotionConfig>
    </AlertDialogPortal>
  );
});
AlertDialogContent.displayName = "AlertDialogContent";

function AlertDialogHeader({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn("flex flex-col gap-2 text-left", className)}
      {...props}
    />
  );
}

function AlertDialogMedia({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn(
        "flex size-11 shrink-0 items-center justify-center rounded-full bg-[color:color-mix(in_oklch,var(--adlg-destructive),transparent_90%)] text-[color:var(--adlg-destructive)]",
        className
      )}
      {...props}
    />
  );
}

function AlertDialogFooter({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn(
        "flex flex-col gap-2 pt-1 sm:flex-row sm:justify-end",
        className
      )}
      {...props}
    />
  );
}

const AlertDialogTitle = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Title>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => {
  return (
    <AlertDialogPrimitive.Title
      className={cn(
        "font-semibold text-[1.35rem] text-[color:var(--adlg-foreground)] leading-tight tracking-[-0.03em]",
        className
      )}
      ref={ref}
      {...props}
    />
  );
});
AlertDialogTitle.displayName = "AlertDialogTitle";

const AlertDialogDescription = React.forwardRef<
  React.ElementRef<typeof AlertDialogPrimitive.Description>,
  React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => {
  return (
    <AlertDialogPrimitive.Description
      className={cn(
        "max-w-[46ch] text-[15px] text-[color:var(--adlg-muted-foreground)] leading-6",
        className
      )}
      ref={ref}
      {...props}
    />
  );
});
AlertDialogDescription.displayName = "AlertDialogDescription";

type AlertDialogCloseRef = React.ComponentProps<
  typeof AlertDialogPrimitive.Close
>["ref"];

type AlertDialogActionVariant = "default" | "destructive";

interface AlertDialogActionProps extends AlertDialogButtonProps {
  closeOnClick?: boolean;
  variant?: AlertDialogActionVariant;
}

const AlertDialogCancel = React.forwardRef<
  HTMLElement,
  AlertDialogButtonProps & { closeOnClick?: boolean }
>(
  (
    {
      asChild,
      children,
      className,
      closeOnClick = true,
      onClick,
      type = "button",
      ...props
    },
    ref
  ) => {
    return (
      <AlertDialogPrimitive.Close
        {...props}
        className={cn(alertDialogCancelClassName, className)}
        onClick={(event) => {
          onClick?.(event);

          if (!closeOnClick) {
            event.preventDefault();
          }
        }}
        ref={ref as AlertDialogCloseRef}
        render={asChild ? <Slot /> : undefined}
        type={asChild ? undefined : type}
      >
        {children}
      </AlertDialogPrimitive.Close>
    );
  }
);
AlertDialogCancel.displayName = "AlertDialogCancel";

const AlertDialogAction = React.forwardRef<HTMLElement, AlertDialogActionProps>(
  (
    {
      asChild,
      children,
      className,
      closeOnClick = true,
      onClick,
      type = "button",
      variant = "destructive",
      ...props
    },
    ref
  ) => {
    const actionClassName =
      variant === "default"
        ? alertDialogDefaultActionClassName
        : alertDialogActionClassName;

    return (
      <AlertDialogPrimitive.Close
        {...props}
        className={cn(actionClassName, className)}
        onClick={(event) => {
          onClick?.(event);

          if (!closeOnClick) {
            event.preventDefault();
          }
        }}
        ref={ref as AlertDialogCloseRef}
        render={asChild ? <Slot /> : undefined}
        type={asChild ? undefined : type}
      >
        {children}
      </AlertDialogPrimitive.Close>
    );
  }
);
AlertDialogAction.displayName = "AlertDialogAction";

export {
  AlertDialog,
  AlertDialogAction,
  AlertDialogCancel,
  AlertDialogContent,
  AlertDialogDescription,
  AlertDialogFooter,
  AlertDialogHeader,
  AlertDialogMedia,
  AlertDialogPortal,
  AlertDialogTitle,
  AlertDialogTrigger,
  alertDialogActionClassName,
  alertDialogCancelClassName,
  alertDialogDefaultActionClassName,
  alertDialogThemeClassName,
  alertDialogTriggerClassName,
  alertDialogTriggerSmClassName,
};

More from Iconiq

Browse all components in this library →