Dialogs
Dialogs provide important prompts in a user flow. They inform users about critical information, require decisions, or involve multiple tasks.
Dialogs focus user attention exclusively on one task or a piece of information via a modal overlay. Expressive dialogs use spring-based motion for entrance/exit animations and clear typography hierarchy.
Introduction
The MD3 Expressive Dialog is a versatile modal component that ranges from simple confirmation alerts to complex full-screen forms. It leverages Radix UI for robust accessibility and Framer Motion for premium, critically-damped spring animations that make the UI feel alive and responsive.
Anatomy
-
Scrim (Overlay): A semi-transparent background that dims the underlying UI.
-
Container: The panel holding the content. Uses rounded corners (28dp) to match the MD3 expressive language.
-
Icon (Optional): A visual indicator placed at the top for emphasis (e.g., Warning, Success).
-
Title: A clear, concise heading describing the purpose.
-
Description / Body: The main content area, which can be scrollable.
-
Footer / Actions: Buttons for the user to confirm or dismiss the prompt.
Variants
Basic Dialog
A standard modal for simple messages or confirmations.
Icon Dialog
Adds an icon at the top for additional context and visual weight.
Full-screen Dialog
Covers the entire viewport. Best for complex mobile-first tasks like creating a new record or extensive form filling.
Features
Scrollable Body
Use DialogBody to create a scrollable region while keeping the title and footer sticky.
Expressive Motion
Dialogs use a spring animation that feels "emphasized" and tactile, transitioning smoothly from a slightly scaled-down state to full size.
Usage
Basic Usage
import { Button } from "@bug-on/m3-expressive/buttons";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger } from "@bug-on/m3-expressive/overlays";
export function Example() {
return (
<Dialog>
<DialogTrigger asChild>
<Button>Open Dialog</Button>
</DialogTrigger>
<DialogPortal>
<DialogOverlay />
<DialogContent>
<DialogTitle>Confirm Action</DialogTitle>
<DialogDescription>
Are you sure you want to proceed with this operation?
</DialogDescription>
<DialogFooter>
<Button colorStyle="text">Cancel</Button>
<Button>Confirm</Button>
</DialogFooter>
</DialogContent>
</DialogPortal>
</Dialog>
);
}
Best Practices
Do
- Use Dialogs for critical information that requires a decision.
- Keep the title concise and the description focused.
- Use Full-screen dialogs for complex tasks on mobile devices.
- Ensure the primary action is clear (e.g., using a high-emphasis button).
Don't
- Don't use a Dialog for information that is already visible on the main screen.
- Don't use too many Dialogs in a single user flow; it can be disruptive.
- Avoid long-running tasks inside a Dialog unless you provide a clear progress indicator.
Accessibility
- Keyboard:
Tabcycles focus inside the dialog.Esccloses the dialog. - Roles: Uses
role="dialog"orrole="alertdialog"depending on content. - Linking: Automatically links
DialogTitleandDialogDescriptionvia ARIA attributes. - Restoration: Focus is restored to the trigger element when the dialog closes.
API Reference
Dialog
Root component.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Controlled open state. |
onOpenChange | (open: boolean) => void | — | Open state change callback. |
DialogContent
The main container.
| Prop | Type | Default | Description |
|---|---|---|---|
hideCloseButton | boolean | false | Hides the default top-right close button. |
closeButtonProps | Partial<IconButtonProps> | — | Override any IconButton prop on the default close button. Useful for disabled, aria-label (i18n), colorStyle, or className. |
closeButton | React.ReactNode | — | Fully replace the close button with a custom node. Auto-wrapped in DialogClose. When provided, hideCloseButton and closeButtonProps are ignored. |
className | string | — | Custom classes on the container. |
Close button customization examples
// Disable during form submission (most common use case)
<DialogContent closeButtonProps={{ disabled: isSubmitting }}>
// i18n label
<DialogContent closeButtonProps={{ "aria-label": "Đóng" }}>
// Restore the original filled style
<DialogContent closeButtonProps={{ colorStyle: "filled" }}>
// Full slot override (custom element, placed inside a flex header)
<DialogContent
closeButton={
<IconButton
disabled={isSubmitting}
aria-label="Đóng"
className="absolute right-4 top-4"
>
<Icon name="close" />
</IconButton>
}
>
DialogFullScreenContent
| Prop | Type | Default | Description |
|---|---|---|---|
title | string | — | Header title in the Top App Bar. |
actionLabel | string | — | Label for the primary action button. |
onAction | () => void | — | Primary action callback. |
actionButtonProps | React.ButtonHTMLAttributes<HTMLButtonElement> | — | Override any attribute on the action button. Common: disabled during form submission. |
closeButtonProps | Partial<IconButtonProps> | — | Override any IconButton prop on the close button. Common: disabled, aria-label for i18n. |
showDivider | boolean | false | Renders a divider below the Top App Bar. |
Full-screen form submission example
<DialogFullScreenContent
title="Tạo lớp học"
actionLabel="Lưu"
onAction={handleSubmit}
actionButtonProps={{ disabled: isSubmitting }}
closeButtonProps={{ disabled: isSubmitting, "aria-label": "Đóng" }}
>
<form onSubmit={handleSubmit}>…</form>
</DialogFullScreenContent>