ModalPreview
Dialog overlay for focused user interactions and important content
Import
import { Modal, useModalState } from '@heroui/react';Usage
"use client";
import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";
export function Default() {
return (
<Modal>
<Button>Open Modal</Button>
<Modal.Overlay>
<Modal.Container>
<Modal.Dialog className="sm:max-w-[360px]">
<Modal.CloseTrigger />
<Modal.Header>
<div className="bg-default ring-muted/25 flex size-10 items-center justify-center rounded-full ring-1">
<Icon className="size-5" icon="gravity-ui:rocket" />
</div>
<h2 className="text-foreground text-lg font-semibold leading-6">Welcome to HeroUI</h2>
</Modal.Header>
<Modal.Body>
<p>
Beautiful, fast and modern React UI library for building accessible and customizable
web applications.
</p>
</Modal.Body>
<Modal.Footer>
<Button className="w-full">Continue</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
);
}Anatomy
<Modal>
<Modal.Trigger /> {/* Or any trigger element */}
<Modal.Overlay> {/* Backdrop */}
<Modal.Container> {/* Positioning container */}
<Modal.Dialog> {/* Content wrapper */}
<Modal.CloseTrigger />
<Modal.Header />
<Modal.Body />
<Modal.Footer />
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>Placement
"use client";
import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";
export function Placements() {
const placements = ["auto", "top", "center", "bottom"] as const;
return (
<div className="flex flex-wrap gap-4">
{placements.map((placement) => (
<Modal key={placement}>
<Button>{placement.charAt(0).toUpperCase() + placement.slice(1)}</Button>
<Modal.Overlay>
<Modal.Container placement={placement}>
<Modal.Dialog className="max-w-[360px]">
<Modal.CloseTrigger />
<Modal.Header>
<div className="bg-default flex size-10 items-center justify-center rounded-full">
<Icon icon="gravity-ui:rocket" />
</div>
<h2 className="text-foreground text-lg font-semibold leading-6">
Welcome to HeroUI
</h2>
</Modal.Header>
<Modal.Body>
<p>
Beautiful, fast and modern React UI library for building accessible and
customizable web applications.
</p>
</Modal.Body>
<Modal.Footer>
<Button className="w-full">Continue</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
))}
</div>
);
}Overlay Variants
"use client";
import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";
export function OverlayVariants() {
const variants = ["solid", "blur", "transparent"] as const;
return (
<div className="flex flex-wrap gap-4">
{variants.map((variant) => (
<Modal key={variant}>
<Button>{variant.charAt(0).toUpperCase() + variant.slice(1)}</Button>
<Modal.Overlay variant={variant}>
<Modal.Container>
<Modal.Dialog className="max-w-[360px]">
<Modal.CloseTrigger />
<Modal.Header>
<div className="bg-default flex size-10 items-center justify-center rounded-full">
<Icon icon="gravity-ui:rocket" />
</div>
<h2 className="text-foreground text-lg font-semibold leading-6">
Welcome to HeroUI
</h2>
</Modal.Header>
<Modal.Body>
<p>
Beautiful, fast and modern React UI library for building accessible and
customizable web applications.
</p>
</Modal.Body>
<Modal.Footer>
<Button className="w-full">Continue</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
))}
</div>
);
}Dismiss Behavior
isDismissable
Controls whether the modal can be dismissed by clicking the overlay backdrop. When set to true, clicking outside the modal will close it.
isKeyboardDismissDisabled
Controls whether the ESC key can dismiss the modal. When set to true, the ESC key will be disabled and won't close the modal.
"use client";
import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";
export function DismissBehavior() {
return (
<div className="flex max-w-sm flex-col gap-6">
<div className="flex flex-col gap-2">
<h3 className="text-lg font-semibold">isDismissable</h3>
<p className="text-muted text-sm">
Controls whether the modal can be dismissed by clicking the overlay backdrop. When set to{" "}
<code>true</code>, clicking outside the modal will close it.
</p>
<Modal>
<Button>Open Modal</Button>
<Modal.Overlay isDismissable>
<Modal.Container>
<Modal.Dialog className="max-w-[360px]">
<Modal.CloseTrigger />
<Modal.Header>
<div className="bg-default ring-muted/25 flex size-10 items-center justify-center rounded-full ring-1">
<Icon className="size-5" icon="gravity-ui:circle-info" />
</div>
<h2 className="text-foreground text-lg font-semibold leading-6">
isDismissable = true
</h2>
<p className="text-muted text-sm leading-5">
Click the overlay backdrop to close
</p>
</Modal.Header>
<Modal.Body>
<p>Click anywhere outside this modal (on the dark overlay) to dismiss it.</p>
</Modal.Body>
<Modal.Footer>
<Button className="w-full">Close</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
</div>
<div className="flex flex-col gap-2">
<h3 className="text-lg font-semibold">isKeyboardDismissDisabled</h3>
<p className="text-muted text-sm">
Controls whether the ESC key can dismiss the modal. When set to <code>true</code>, the ESC
key will be disabled and won't close the modal.
</p>
<Modal>
<Button>Open Modal</Button>
<Modal.Overlay isKeyboardDismissDisabled>
<Modal.Container>
<Modal.Dialog className="max-w-[360px]">
<Modal.CloseTrigger />
<Modal.Header>
<div className="bg-default ring-muted/25 flex size-10 items-center justify-center rounded-full ring-1">
<Icon className="size-5" icon="gravity-ui:circle-info" />
</div>
<h2 className="text-foreground text-lg font-semibold leading-6">
isKeyboardDismissDisabled = true
</h2>
<p className="text-muted text-sm leading-5">ESC key is disabled</p>
</Modal.Header>
<Modal.Body>
<p>
Press ESC - it won't close this modal. Use the close button or click the
overlay.
</p>
</Modal.Body>
<Modal.Footer>
<Button className="w-full">Close</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
</div>
</div>
);
}Scroll Behavior
"use client";
import {Button, Modal} from "@heroui/react";
import {useState} from "react";
export function ScrollBehavior() {
const [scroll, setScroll] = useState<"inside" | "outside">("inside");
return (
<div className="flex flex-col gap-4">
<div className="flex gap-4">
<label className="flex items-center gap-2">
<input
checked={scroll === "inside"}
name="scroll"
type="radio"
value="inside"
onChange={(e) => setScroll(e.target.value as "inside" | "outside")}
/>
Inside
</label>
<label className="flex items-center gap-2">
<input
checked={scroll === "outside"}
name="scroll"
type="radio"
value="outside"
onChange={(e) => setScroll(e.target.value as "inside" | "outside")}
/>
Outside
</label>
</div>
<Modal>
<Button>Open Modal ({scroll.charAt(0).toUpperCase() + scroll.slice(1)})</Button>
<Modal.Overlay>
<Modal.Container scroll={scroll}>
<Modal.Dialog>
<Modal.Header>
<h2 className="text-foreground text-lg font-semibold leading-6">
Scroll: {scroll.charAt(0).toUpperCase() + scroll.slice(1)}
</h2>
<p className="text-muted text-sm leading-5">
Toggle the radio buttons above to see different scroll behaviors
</p>
</Modal.Header>
<Modal.Body>
{Array.from({length: 30}).map((_, i) => (
<p key={i} className="mb-3">
Paragraph {i + 1}: Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet
hendrerit risus, sed porttitor quam.
</p>
))}
</Modal.Body>
<Modal.Footer>
<Button variant="secondary">Cancel</Button>
<Button>Confirm</Button>
</Modal.Footer>
<Modal.CloseTrigger />
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
</div>
);
}With Form
"use client";
import {Button, Input, Label, Modal, TextField} from "@heroui/react";
import {Icon} from "@iconify/react";
export function WithForm() {
return (
<Modal>
<Button>Open Contact Form</Button>
<Modal.Overlay>
<Modal.Container placement="auto">
<Modal.Dialog className="sm:max-w-md">
<Modal.CloseTrigger />
<Modal.Header>
<div className="bg-accent-soft text-accent-soft-foreground flex size-10 items-center justify-center rounded-full">
<Icon className="size-5" icon="gravity-ui:envelope" />
</div>
<h2 className="text-foreground text-lg font-semibold leading-6">Contact Us</h2>
<p className="text-muted text-sm leading-5">
Fill out the form below. On mobile, the modal will adapt when the keyboard appears.
</p>
</Modal.Header>
<Modal.Body>
<form className="flex flex-col gap-4">
<TextField className="w-full" name="name" type="text">
<Label>Name</Label>
<Input placeholder="Enter your name" />
</TextField>
<TextField className="w-full" name="email" type="email">
<Label>Email</Label>
<Input placeholder="Enter your email" />
</TextField>
<TextField className="w-full" name="phone" type="tel">
<Label>Phone</Label>
<Input placeholder="Enter your phone number" />
</TextField>
<TextField className="w-full" name="company">
<Label>Company</Label>
<Input placeholder="Enter your company name" />
</TextField>
<TextField className="w-full" name="message">
<Label>Message</Label>
<Input placeholder="Enter your message" />
</TextField>
</form>
</Modal.Body>
<Modal.Footer>
<Modal.CloseTrigger asChild>
<Button variant="secondary">Cancel</Button>
</Modal.CloseTrigger>
<Button>Send Message</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
);
}Controlled State
Modal State
Status: closed
"use client";
import {Button, Modal, useModalState} from "@heroui/react";
import {Icon} from "@iconify/react";
export function Controlled() {
const modalState = useModalState();
return (
<div className="flex flex-col gap-4">
<div className="border-border bg-default rounded-lg border p-4">
<p className="mb-2 text-sm font-medium">Modal State</p>
<p className="text-muted text-sm">
Status: <span className="font-mono">{modalState.isOpen ? "open" : "closed"}</span>
</p>
</div>
<div className="flex flex-wrap gap-3">
<Button onPress={modalState.open}>Open Modal</Button>
<Button variant="secondary" onPress={modalState.toggle}>
Toggle Modal
</Button>
</div>
<Modal state={modalState}>
<Modal.Overlay>
<Modal.Container>
<Modal.Dialog>
<Modal.Header>
<div className="bg-accent-soft text-accent-soft-foreground flex size-10 items-center justify-center rounded-full">
<Icon className="size-5" icon="gravity-ui:circle-check" />
</div>
<h2 className="text-foreground text-lg font-semibold leading-6">
Controlled with useModalState()
</h2>
<p className="text-muted text-sm leading-5">
This modal is controlled programmatically using the useModalState() hook
</p>
</Modal.Header>
<Modal.Body>
<p>
The hook provides methods like <code>open()</code>, <code>close()</code>,{" "}
<code>toggle()</code>, and access to <code>isOpen</code> state.
</p>
<p className="mt-3">
This enables powerful patterns like opening modals from anywhere in your
component, multiple triggers, and external state management.
</p>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onPress={modalState.close}>
Close
</Button>
<Button onPress={modalState.close}>Confirm</Button>
</Modal.Footer>
<Modal.CloseTrigger />
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
</div>
);
}Custom Trigger
Settings
Click to open settings
"use client";
import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";
export function CustomTrigger() {
return (
<Modal>
<Modal.Trigger>
<div className="border-border bg-default hover:bg-default-hover flex cursor-pointer items-center gap-3 rounded-lg border p-4 transition-colors">
<div className="flex size-10 items-center justify-center">
<Icon className="text-primary size-6" icon="gravity-ui:gear" />
</div>
<div>
<p className="font-medium">Settings</p>
<p className="text-muted text-sm">Click to open settings</p>
</div>
</div>
</Modal.Trigger>
<Modal.Overlay>
<Modal.Container>
<Modal.Dialog>
{({close}) => (
<>
<Modal.Header>
<div className="bg-accent-soft text-accent-soft-foreground flex size-10 items-center justify-center rounded-full">
<Icon className="size-5" icon="gravity-ui:gear" />
</div>
<h2 className="text-foreground text-lg font-semibold leading-6">Settings</h2>
</Modal.Header>
<Modal.Body>
<p>Using Modal.Trigger allows you to create custom trigger elements.</p>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onPress={close}>
Cancel
</Button>
<Button onPress={close}>Save</Button>
</Modal.Footer>
<Modal.CloseTrigger />
</>
)}
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
);
}Custom Overlay
"use client";
import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";
export function CustomOverlay() {
return (
<Modal>
<Button>Custom Backdrop</Button>
<Modal.Overlay
className="bg-gradient-to-t from-black/80 via-black/40 to-transparent dark:from-zinc-800/80 dark:via-zinc-800/40"
variant="blur"
>
<Modal.Container>
<Modal.Dialog>
<Modal.Header className="items-center text-center">
<div className="bg-accent-soft text-accent-soft-foreground flex size-10 items-center justify-center rounded-full">
<Icon className="size-5" icon="gravity-ui:sparkles" />
</div>
<h2 className="text-foreground text-lg font-semibold leading-6">Premium Backdrop</h2>
<p className="text-muted text-sm leading-5">
Elegant gradient adapts to light and dark modes
</p>
</Modal.Header>
<Modal.Body>
<p>
This backdrop features a sophisticated gradient that transitions from an elegant
dark color at the bottom to complete transparency at the top, combined with a smooth
blur effect. The gradient automatically adapts its intensity for optimal contrast in
both light and dark modes.
</p>
</Modal.Body>
<Modal.Footer className="flex-col-reverse">
<Button className="w-full">Amazing!</Button>
<Button className="w-full" variant="secondary">
Close
</Button>
</Modal.Footer>
<Modal.CloseTrigger />
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
);
}Custom Animations
"use client";
import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";
import clsx from "clsx";
export function CustomAnimations() {
const animations = {
"Blur Slide": [
"data-[entering]:blur-in-8",
"data-[entering]:slide-in-from-bottom-8",
"data-[entering]:fade-in-0",
"data-[exiting]:blur-out-8",
"data-[exiting]:slide-out-to-bottom-8",
"data-[exiting]:fade-out",
].join(" "),
Bounce: [
"data-[entering]:zoom-in-50",
"data-[entering]:fade-in-0",
"data-[entering]:ease-[cubic-bezier(0.68,-0.55,0.265,1.55)]",
"data-[exiting]:zoom-out-95",
"data-[exiting]:fade-out",
"data-[exiting]:duration-200",
].join(" "),
"Rotate Zoom": [
"data-[entering]:zoom-in-75",
"data-[entering]:spin-in-45",
"data-[entering]:fade-in-0",
"data-[exiting]:zoom-out-95",
"data-[exiting]:spin-out-12",
"data-[exiting]:fade-out",
].join(" "),
Spring: [
"data-[entering]:zoom-in-90",
"data-[entering]:ease-spring",
"data-[exiting]:zoom-out-95",
"data-[exiting]:ease-out-quart",
].join(" "),
};
return (
<div className="flex flex-wrap gap-4">
{Object.entries(animations).map(([name, classNames]) => (
<Modal key={name}>
<Button>{name}</Button>
<Modal.Overlay className="data-[entering]:duration-500 data-[exiting]:duration-200">
<Modal.Container
className={clsx(
classNames,
"data-[entering]:animate-in",
"data-[exiting]:animate-out",
"data-[entering]:duration-300",
"data-[exiting]:duration-150",
)}
>
<Modal.Dialog className="sm:max-w-[360px]">
<Modal.CloseTrigger />
<Modal.Header>
<div className="bg-default ring-muted/25 flex size-10 items-center justify-center rounded-full ring-1">
<Icon className="size-5" icon="gravity-ui:rocket" />
</div>
<h2 className="text-foreground text-lg font-semibold leading-6">
Welcome to HeroUI
</h2>
</Modal.Header>
<Modal.Body>
<p>
Beautiful, fast and modern React UI library for building accessible and
customizable web applications.
</p>
</Modal.Body>
<Modal.Footer>
<Button className="w-full">Continue</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>
))}
</div>
);
}Styling
Passing Tailwind CSS classes
<Modal>
<Button>Open</Button>
<Modal.Overlay className="bg-black/80">
<Modal.Container className="items-start pt-20">
<Modal.Dialog className="bg-gradient-to-br from-purple-500 to-pink-500 text-white">
<Modal.Header className="border-b border-white/20">
<h2>Custom Styled Modal</h2>
</Modal.Header>
<Modal.Body>Content here</Modal.Body>
</Modal.Dialog>
</Modal.Container>
</Modal.Overlay>
</Modal>Customizing with CSS
@layer components {
/* Custom overlay */
.modal__overlay {
@apply bg-gradient-to-br from-black/50 to-black/70;
}
/* Custom dialog */
.modal__dialog {
@apply border border-white/10 shadow-2xl;
}
/* Custom close button */
.modal__close-trigger {
@apply rounded-full bg-white/10;
}
}CSS Classes
Modal uses BEM naming for styling hooks.
Component Classes
| Class | Description |
|---|---|
modal__trigger | Trigger element wrapper |
modal__overlay | Backdrop overlay |
modal__container | Positioning container |
modal__dialog | Content wrapper |
modal__header | Header section |
modal__body | Body content area |
modal__footer | Footer section |
modal__close-trigger | Close button |
Variant Classes
| Class | Description |
|---|---|
modal__overlay--solid | Solid black backdrop |
modal__overlay--blur | Blurred backdrop |
modal__overlay--transparent | No backdrop |
modal__container--scroll-outside | Scroll entire modal |
modal__dialog--scroll-inside | Scroll body only |
modal__dialog--scroll-outside | Dialog for outside scroll |
modal__body--scroll-inside | Scrollable body |
modal__body--scroll-outside | Non-scrollable body |
Interactive States
Modal components support standard interaction states:
/* Trigger focus */
.modal__trigger:focus-visible { }
.modal__trigger[data-focus-visible="true"] { }
/* Close button hover */
.modal__close-trigger:hover { }
.modal__close-trigger[data-hover="true"] { }
/* Close button pressed */
.modal__close-trigger:active { }
.modal__close-trigger[data-pressed="true"] { }
/* Dialog focus */
.modal__dialog:focus-visible { }
.modal__dialog[data-focus-visible="true"] { }
/* Animation states */
.modal__overlay[data-entering] { }
.modal__overlay[data-exiting] { }
.modal__container[data-entering] { }
.modal__container[data-exiting] { }API Reference
Modal Props
| Prop | Type | Default | Description |
|---|---|---|---|
state | UseModalStateReturn | - | External state from useModalState hook |
defaultOpen | boolean | false | Initially open state |
isOpen | boolean | - | Controlled open state |
onOpenChange | (isOpen: boolean) => void | - | Called when open state changes |
Modal.Trigger Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Trigger content |
className | string | - | Additional CSS classes |
Modal.Overlay Props
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "solid" | "blur" | "transparent" | "solid" | Backdrop style |
className | string | (values) => string | - | Additional CSS classes |
isDismissable | boolean | false | Close on backdrop click |
isKeyboardDismissDisabled | boolean | true | Disable ESC key |
Modal.Container Props
| Prop | Type | Default | Description |
|---|---|---|---|
placement | "auto" | "center" | "top" | "bottom" | "auto" | Viewport position |
scroll | "inside" | "outside" | "inside" | Scroll behavior |
className | string | (values) => string | - | Additional CSS classes |
Modal.Dialog Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ({close}) => ReactNode | - | Content or render function |
className | string | (values) => string | - | Additional CSS classes |
role | string | "dialog" | ARIA role |
aria-label | string | - | Accessibility label |
aria-labelledby | string | - | ID of labelling element |
aria-describedby | string | - | ID of describing element |
Modal.Body Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Body content |
className | string | - | Additional CSS classes |
Modal.Header Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Header content |
className | string | - | Additional CSS classes |
Modal.Footer Props
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | Footer content |
className | string | - | Additional CSS classes |
Modal.CloseTrigger Props
| Prop | Type | Default | Description |
|---|---|---|---|
asChild | boolean | false | Render as child element |
children | ReactNode | - | Custom close button |
className | string | (values) => string | - | Additional CSS classes |
Hooks
const modalState = useModalState({
defaultOpen: false,
onOpenChange: (isOpen) => console.log('Modal:', isOpen)
});
// Available methods
modalState.isOpen // Current state
modalState.open() // Open modal
modalState.close() // Close modal
modalState.toggle() // Toggle state
modalState.setOpen() // Set state directlyAccessibility
Modal implements WAI-ARIA Dialog pattern with:
- Focus Management: Focus trapped within modal when open
- Keyboard Navigation:
Escapecloses modal (when dismissable)Tabcycles through interactive elements- Focus returns to trigger on close
- Screen Readers: Proper ARIA attributes and live regions
- Scroll Lock: Body scroll disabled when modal open
- Click Outside: Closes modal when clicking backdrop (configurable)