Devie UI
Version: 2026-02-14

The AlertDialog component extends the Base UI Alert Dialog with additional structural sub-components: AlertDialog.Header, AlertDialog.Body, and AlertDialog.Footer. It also adds a disableInteractions prop on the root to temporarily make the dialog content inert during async actions.

Installation

Here is the component implementation code that you can copy to your project:

// https://devie-ui.com/components/alert-dialog
// https://base-ui.com/react/components/alert-dialog
 
"use client";
 
import { AlertDialog as BaseAlertDialog } from "@base-ui/react/alert-dialog";
import clsx from "clsx";
import * as React from "react";
import styles from "./AlertDialog.module.scss";
 
const AlertDialogInteractionContext = React.createContext<boolean | null>(null);
 
function Root({ disableInteractions, ...props }: AlertDialog.Root.Props) {
  return (
    <AlertDialogInteractionContext.Provider value={disableInteractions ?? null}>
      <BaseAlertDialog.Root {...props} />
    </AlertDialogInteractionContext.Provider>
  );
}
const createHandle = BaseAlertDialog.createHandle;
 
function Trigger({ className, ...props }: BaseAlertDialog.Trigger.Props) {
  return (
    <BaseAlertDialog.Trigger
      className={clsx(styles.trigger, className)}
      {...props}
    />
  );
}
 
const Portal = BaseAlertDialog.Portal;
 
function Close({ className, render, ...props }: BaseAlertDialog.Close.Props) {
  return (
    <BaseAlertDialog.Close
      className={clsx(!render && styles.close, className)}
      render={render}
      {...props}
    />
  );
}
 
function Backdrop({ className, ...props }: BaseAlertDialog.Backdrop.Props) {
  return (
    <BaseAlertDialog.Backdrop
      className={clsx(styles.backdrop, className)}
      {...props}
    />
  );
}
 
function Popup({
  className,
  disableInteractions,
  ...props
}: AlertDialog.Popup.Props) {
  const contextDisableInteractions = React.useContext(
    AlertDialogInteractionContext,
  );
  const resolvedDisableInteractions =
    disableInteractions ?? contextDisableInteractions ?? false;
 
  return (
    <BaseAlertDialog.Popup
      className={clsx(
        styles.popup,
        resolvedDisableInteractions && styles.popupNonInteractive,
        className,
      )}
      aria-busy={resolvedDisableInteractions ? true : undefined}
      inert={resolvedDisableInteractions ? true : undefined}
      {...props}
    />
  );
}
 
function Title({ className, ...props }: BaseAlertDialog.Title.Props) {
  return (
    <BaseAlertDialog.Title
      className={clsx(styles.title, className)}
      render={({ children }) => (
        <h3 className={clsx(styles.title, className)} {...props}>
          {children}
        </h3>
      )}
      {...props}
    />
  );
}
 
function Description({
  className,
  ...props
}: BaseAlertDialog.Description.Props) {
  return (
    <BaseAlertDialog.Description
      className={clsx(styles.description, className)}
      {...props}
    />
  );
}
 
function Header({ className, ...props }: AlertDialog.Header.Props) {
  return <div className={clsx(styles.header, className)} {...props} />;
}
 
function Footer({ className, ...props }: AlertDialog.Footer.Props) {
  return <div className={clsx(styles.footer, className)} {...props} />;
}
 
function Body({ className, ...props }: AlertDialog.Body.Props) {
  return <div className={clsx(styles.body, className)} {...props} />;
}
 
const AlertDialog = {
  Root,
  createHandle,
  Trigger,
  Portal,
  Close,
  Backdrop,
  Popup,
  Header,
  Footer,
  Title,
  Description,
  Body,
};
 
namespace AlertDialog {
  export namespace Root {
    export interface Props extends BaseAlertDialog.Root.Props {
      disableInteractions?: boolean;
    }
  }
  export namespace Trigger {
    export type Props = BaseAlertDialog.Trigger.Props;
  }
  export namespace Portal {
    export type Props = BaseAlertDialog.Portal.Props;
  }
  export namespace Close {
    export type Props = BaseAlertDialog.Close.Props;
  }
  export namespace Backdrop {
    export type Props = BaseAlertDialog.Backdrop.Props;
  }
  export namespace Popup {
    export interface Props extends BaseAlertDialog.Popup.Props {
      disableInteractions?: boolean;
    }
  }
  export namespace Title {
    export type Props = BaseAlertDialog.Title.Props;
  }
  export namespace Description {
    export type Props = BaseAlertDialog.Description.Props;
  }
  export namespace Header {
    export interface Props extends React.HTMLAttributes<HTMLDivElement> {
      className?: string;
    }
  }
  export namespace Footer {
    export interface Props extends React.HTMLAttributes<HTMLDivElement> {
      className?: string;
    }
  }
  export namespace Body {
    export interface Props extends React.HTMLAttributes<HTMLDivElement> {
      className?: string;
    }
  }
}
 
export default AlertDialog;

Use Cases

Opening with a Trigger component

The most common way to open an AlertDialog is by using the AlertDialog.Trigger component. Place it inside the AlertDialog.Root and it will automatically handle opening the dialog when clicked.

Opening with a detached Trigger

When defining the AlertDialog content next to its trigger is not practical, you can use a detached trigger with AlertDialog.createHandle(). This allows you to place the trigger button anywhere in your component tree while still controlling the same dialog instance.

Opening programmatically with state

For complete control over the dialog's visibility, use the open and onOpenChange props on AlertDialog.Root. This is useful when you need to open the dialog based on application logic, such as after an API call or in response to a menu action.

With a form and async submission

AlertDialogs can contain forms with validation. This example shows how to handle async form submission with a loading state on the submit button while using disableInteractions to keep the dialog content inert until the operation completes.

With Select dropdowns

AlertDialogs can contain Select components for dropdown selections. However, there's a caveat: when a Select is inside a dialog (or any scrollable container with overflow-y: auto), the dropdown may display excessive white space below the last item.

This happens because Base UI's Select.Positioner defaults to alignItemWithTrigger={true}, which calculates available height based on viewport boundaries rather than the container's bounds.

The fix: Pass alignItemWithTrigger={false} to Select.Positioner. This disables the "selected item aligns with trigger" UX behavior (the popup appears below/above instead of overlapping), but resolves the height calculation issue.

Minimal design without Header/Footer

For simpler use cases like notifications or quick confirmations, you can omit the AlertDialog.Header, AlertDialog.Body, and AlertDialog.Footer components entirely. Apply custom padding and positioning directly on the AlertDialog.Popup and use AlertDialog.Close with an icon for a clean, minimal look.