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. Add enableFadingBlur to DialogHeader and/or DialogFooter to fade and blur content at the scroll boundary without intercepting interaction.
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. |
motion | "auto" | "always" | "off" | "auto" | Controls library-authored open/close motion. auto follows the OS reduced-motion preference. |
DialogContent
The main container.
| Prop | Type | Default | Description |
|---|---|---|---|
hideCloseButton | boolean | false | Hides the default top-right close button. |
motion | "auto" | "always" | "off" | inherited | Local override for the content transition. |
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. |
DialogHeader and DialogFooter
Both layout slots accept standard <div> props plus the following opt-in fading-blur props. The header mask fades down into DialogBody; the footer mask fades upward. The default dialog tint is var(--md-sys-color-surface-container-high).
| Prop | Type | Default | Description |
|---|---|---|---|
enableFadingBlur | boolean | false | Enables the directional blur and tint mask at this scroll boundary. |
blurIntensity | number | 12 | Backdrop blur radius in pixels. |
blurHeight | number | string | 48 | Height of the fading mask. |
fadingBlurColor | string | Dialog surface container | Custom tint color for the mask. |
<DialogHeader enableFadingBlur>
<DialogTitle>Terms of Service</DialogTitle>
</DialogHeader>
<DialogBody>{/* long, scrollable content */}</DialogBody>
<DialogFooter enableFadingBlur>
<Button>Accept</Button>
</DialogFooter>
DialogBody content layout
DialogBody keeps className for the scroll container (for example, sizing
with max-h-75). Use contentClassName for classes that should style the
children inside the scrollable content, such as space-y-*, flex, or
grid. The library provides the wrapper automatically, so no extra <div> is
needed.
<DialogBody
className="max-h-75"
contentClassName="space-y-4"
>
<TextField label="First name" />
<TextField label="Last name" />
</DialogBody>
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. |
enableFadingBlur | boolean | false | Enables a top-edge fading blur over the full-screen scroll area. |
blurIntensity | number | 12 | Backdrop blur radius in pixels. |
blurHeight | number | string | 48 | Height of the fading mask. |
fadingBlurColor | string | Surface | Custom tint color for the top mask. |
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" }}
enableFadingBlur
>
<form onSubmit={handleSubmit}>…</form>
</DialogFullScreenContent>