# Modal **Category**: react **URL**: https://www.heroui.com/docs/react/components/modal **Source**: https://raw.githubusercontent.com/heroui-inc/heroui/refs/heads/v3/apps/docs/content/docs/react/components/(overlays)/modal.mdx > Dialog overlay for focused user interactions and important content *** ## Import ```tsx import { Modal } from "@heroui/react"; ``` ### Usage ```tsx "use client"; import {Rocket} from "@gravity-ui/icons"; import {Button, Modal} from "@heroui/react"; export function Default() { return ( Welcome to HeroUI

A beautiful, fast, and modern React UI library for building accessible and customizable web applications with ease.

); } ``` ### Anatomy Import the Modal component and access all parts using dot notation. ```tsx import {Modal, Button} from "@heroui/react"; export default () => ( {/* Optional: Close button */} {/* Optional: Icon */} ); ``` ### Placement ```tsx "use client"; import {Rocket} from "@gravity-ui/icons"; import {Button, Modal} from "@heroui/react"; export function Placements() { const placements = ["auto", "top", "center", "bottom"] as const; return (
{placements.map((placement) => ( Placement: {placement.charAt(0).toUpperCase() + placement.slice(1)}

This modal uses the {placement} placement option. Try different placements to see how the modal positions itself on the screen.

))}
); } ``` ### Backdrop Variants ```tsx "use client"; import {Rocket} from "@gravity-ui/icons"; import {Button, Modal} from "@heroui/react"; export function BackdropVariants() { const variants = ["opaque", "blur", "transparent"] as const; return (
{variants.map((variant) => ( Backdrop: {variant.charAt(0).toUpperCase() + variant.slice(1)}

This modal uses the {variant} backdrop variant. Compare the different visual effects: opaque provides full opacity, blur adds a backdrop filter, and transparent removes the background.

))}
); } ``` ### Sizes ```tsx "use client"; import {Rocket} from "@gravity-ui/icons"; import {Button, Modal} from "@heroui/react"; export function Sizes() { const sizes = ["xs", "sm", "md", "lg", "cover", "full"] as const; return (
{sizes.map((size) => ( Size: {size.charAt(0).toUpperCase() + size.slice(1)}

{size === "cover" ? ( <> This modal uses the cover size variant. It spans the full screen with margins: 16px on mobile and 40px on desktop. Maintains rounded corners and standard padding. Perfect for cover-style content that needs maximum width while preserving modal aesthetics. ) : size === "full" ? ( <> This modal uses the full size variant. It occupies the entire viewport without any margins, rounded corners, or shadows, creating a true fullscreen experience. Ideal for immersive content or full-page interactions. ) : ( <> This modal uses the {size} size variant. On mobile devices, all sizes adapt to near full-width for optimal viewing. On desktop, each size provides a different maximum width to suit various content needs. )}

))}
); } ``` ### Custom Backdrop ```tsx "use client"; import {Sparkles} from "@gravity-ui/icons"; import {Button, Modal} from "@heroui/react"; export function CustomBackdrop() { return ( Premium Backdrop

This backdrop features a sophisticated gradient that transitions from a 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.

); } ``` ### Dismiss Behavior ```tsx "use client"; import {CircleInfo} from "@gravity-ui/icons"; import {Button, Modal} from "@heroui/react"; export function DismissBehavior() { return (

isDismissable

Controls whether the modal can be dismissed by clicking the overlay backdrop. Defaults to{" "} true. Set to false to require explicit close action.

isDismissable = false

Clicking the backdrop won't close this modal

Try clicking outside this modal on the overlay - it won't close. You must use the close button or press ESC to dismiss it.

isKeyboardDismissDisabled

Controls whether the ESC key can dismiss the modal. When set to true, the ESC key will be disabled and users must use explicit close actions.

isKeyboardDismissDisabled = true

ESC key is disabled

Press ESC - nothing happens. You must use the close button or click the overlay backdrop to dismiss this modal.

); } ``` ### Close Methods ```tsx "use client"; import {CircleCheck, CircleInfo} from "@gravity-ui/icons"; import {Button, Modal} from "@heroui/react"; export function CloseMethods() { return (

Using slot="close"

The simplest way to close a modal. Add slot="close" to any Button component within the modal. When clicked, it will automatically close the modal.

Using slot="close"

Click either button below - both have slot="close" and will close the modal automatically.

Using Dialog render props

Access the close method from the Dialog's render props. This gives you full control over when and how to close the modal, allowing you to add custom logic before closing.

{(renderProps) => ( <> Using Dialog render props

The buttons below use the close method from render props. You can add validation or other logic before calling{" "} renderProps.close().

)}
); } ``` ### Scroll Behavior ```tsx "use client"; import {Button, Label, Modal, Radio, RadioGroup} from "@heroui/react"; import {useState} from "react"; export function ScrollComparison() { const [scroll, setScroll] = useState<"inside" | "outside">("inside"); return (
setScroll(value as "inside" | "outside")} > Scroll: {scroll.charAt(0).toUpperCase() + scroll.slice(1)}

Compare scroll behaviors - inside keeps content scrollable within the modal, outside allows page scrolling

{Array.from({length: 30}).map((_, i) => (

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.

))}
); } ``` ### Controlled State ```tsx "use client"; import {CircleCheck} from "@gravity-ui/icons"; import {Button, Modal, useOverlayState} from "@heroui/react"; import React from "react"; export function Controlled() { const [isOpen, setIsOpen] = React.useState(false); const state = useOverlayState(); return (

With React.useState()

Control the modal using React's useState hook for simple state management. Perfect for basic use cases.

Status:{" "} {isOpen ? "open" : "closed"}

Controlled with useState()

This modal is controlled by React's useState hook. Pass{" "} isOpen and onOpenChange props to manage the modal state externally.

With useOverlayState()

Use the useOverlayState hook for a cleaner API with convenient methods like open(), close(), and{" "} toggle().

Status:{" "} {state.isOpen ? "open" : "closed"}

Controlled with useOverlayState()

The useOverlayState hook provides dedicated methods for common operations. No need to manually create callbacks—just use{" "} state.open(), state.close(), or{" "} state.toggle().

); } ``` ### With Form ```tsx "use client"; import {Envelope} from "@gravity-ui/icons"; import {Button, Input, Label, Modal, Surface, TextField} from "@heroui/react"; export function WithForm() { return ( Contact Us

Fill out the form below and we'll get back to you. The modal adapts automatically when the keyboard appears on mobile.

); } ``` ### Custom Trigger ```tsx "use client"; import {Gear} from "@gravity-ui/icons"; import {Button, Modal} from "@heroui/react"; export function CustomTrigger() { return (

Settings

Manage your preferences

Settings

Use Modal.Trigger to create custom trigger elements beyond standard buttons. This example shows a card-style trigger with icons and descriptive text.

); } ``` ### Custom Animations ```tsx "use client"; import {ArrowUpFromLine, Sparkles} from "@gravity-ui/icons"; import {Button, Modal} from "@heroui/react"; import React from "react"; const iconMap: Record> = { "gravity-ui:arrow-up-from-line": ArrowUpFromLine, "gravity-ui:sparkles": Sparkles, }; export function CustomAnimations() { const animations = [ { classNames: { backdrop: [ "data-[entering]:duration-400", "data-[entering]:ease-[cubic-bezier(0.16,1,0.3,1)]", "data-[exiting]:duration-200", "data-[exiting]:ease-[cubic-bezier(0.7,0,0.84,0)]", ].join(" "), container: [ "data-[entering]:animate-in", "data-[entering]:fade-in-0", "data-[entering]:zoom-in-95", "data-[entering]:duration-400", "data-[entering]:ease-[cubic-bezier(0.16,1,0.3,1)]", "data-[exiting]:animate-out", "data-[exiting]:fade-out-0", "data-[exiting]:zoom-out-95", "data-[exiting]:duration-200", "data-[exiting]:ease-[cubic-bezier(0.7,0,0.84,0)]", ].join(" "), }, description: "Physics-based elastic scaling. Simulates a high-damping spring system with fast transient response and prolonged settling time. Ideal for Modals and Popovers.", icon: "gravity-ui:sparkles", name: "Kinematic Scale", }, { classNames: { backdrop: [ "data-[entering]:duration-500", "data-[entering]:ease-[cubic-bezier(0.25,1,0.5,1)]", "data-[exiting]:duration-200", "data-[exiting]:ease-[cubic-bezier(0.5,0,0.75,0)]", ].join(" "), container: [ "data-[entering]:animate-in", "data-[entering]:fade-in-0", "data-[entering]:slide-in-from-bottom-4", "data-[entering]:duration-500", "data-[entering]:ease-[cubic-bezier(0.25,1,0.5,1)]", "data-[exiting]:animate-out", "data-[exiting]:fade-out-0", "data-[exiting]:slide-out-to-bottom-2", "data-[exiting]:duration-200", "data-[exiting]:ease-[cubic-bezier(0.5,0,0.75,0)]", ].join(" "), }, description: "Simulates movement through a medium with fluid resistance. Eliminates mechanical linearity for a natural, grounded feel. Perfect for Bottom Sheets or Toasts.", icon: "gravity-ui:arrow-up-from-line", name: "Fluid Slide", }, ]; return (
{animations.map(({classNames, description, icon, name}) => { const IconComponent = iconMap[icon]; return ( {!!IconComponent && } {name} Animation

{description}

); })}
); } ``` ### Custom Portal ```tsx "use client"; import {Button, Modal} from "@heroui/react"; import {useCallback, useRef, useState} from "react"; export function CustomPortal() { const portalRef = useRef(null); const [portalContainer, setPortalContainer] = useState(null); const setPortalRef = useCallback((node: HTMLDivElement | null) => { portalRef.current = node; setPortalContainer(node); }, []); return (

Render modals inside a custom container instead of document.body

Apply transform: translateZ(0) to the container to create a new stacking context.

{!!portalContainer && ( Custom Portal

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.

)}
); } ``` ## Styling ### Passing Tailwind CSS classes ```tsx import {Modal, Button} from "@heroui/react"; function CustomModal() { return ( Custom Styled Modal

This modal has custom styling applied via Tailwind classes

); } ``` ### Customizing the component classes To customize the Modal component classes, you can use the `@layer components` directive.
[Learn more](https://tailwindcss.com/docs/adding-custom-styles#adding-component-classes). ```css @layer components { .modal__backdrop { @apply bg-gradient-to-br from-black/50 to-black/70; } .modal__dialog { @apply rounded-2xl border border-white/10 shadow-2xl; } .modal__header { @apply text-center; } .modal__close-trigger { @apply rounded-full bg-white/10 hover:bg-white/20; } } ``` HeroUI follows the [BEM](https://getbem.com/) methodology to ensure component variants and states are reusable and easy to customize. ### CSS Classes The Modal component uses these CSS classes ([View source styles](https://github.com/heroui-inc/heroui/blob/v3/packages/styles/components/modal.css)): #### Base Classes - `.modal__trigger` - Trigger element that opens the modal - `.modal__backdrop` - Overlay backdrop behind the modal - `.modal__container` - Positioning wrapper with placement support - `.modal__dialog` - Modal content container - `.modal__header` - Header section for titles and icons - `.modal__body` - Main content area - `.modal__footer` - Footer section for actions - `.modal__close-trigger` - Close button element #### Backdrop Variants - `.modal__backdrop--opaque` - Opaque colored backdrop (default) - `.modal__backdrop--blur` - Blurred backdrop with glass effect - `.modal__backdrop--transparent` - Transparent backdrop (no overlay) #### Scroll Variants - `.modal__container--scroll-outside` - Enables scrolling the entire modal - `.modal__dialog--scroll-inside` - Constrains modal height for body scrolling - `.modal__body--scroll-inside` - Makes only the body scrollable - `.modal__body--scroll-outside` - Allows full-page scrolling ### Interactive States The component supports these interactive states: - **Focus**: `:focus-visible` or `[data-focus-visible="true"]` - Applied to trigger, dialog, and close button - **Hover**: `:hover` or `[data-hovered="true"]` - Applied to close button on hover - **Active**: `:active` or `[data-pressed="true"]` - Applied to close button when pressed - **Entering**: `[data-entering]` - Applied during modal opening animation - **Exiting**: `[data-exiting]` - Applied during modal closing animation - **Placement**: `[data-placement="*"]` - Applied based on modal position (auto, top, center, bottom) ## API Reference ### Modal | Prop | Type | Default | Description | | ---------- | ----------- | ------- | ------------------------------ | | `children` | `ReactNode` | - | Trigger and container elements | ### Modal.Trigger | Prop | Type | Default | Description | | ----------- | ----------- | ------- | ---------------------- | | `children` | `ReactNode` | - | Custom trigger content | | `className` | `string` | - | CSS classes | ### Modal.Backdrop | Prop | Type | Default | Description | | --------------------------- | ------------------------------------- | ---------- | ------------------------- | | `variant` | `"opaque" \| "blur" \| "transparent"` | `"opaque"` | Backdrop overlay style | | `isDismissable` | `boolean` | `true` | Close on backdrop click | | `isKeyboardDismissDisabled` | `boolean` | `false` | Disable ESC key to close | | `isOpen` | `boolean` | - | Controlled open state | | `onOpenChange` | `(isOpen: boolean) => void` | - | Open state change handler | | `className` | `string \| (values) => string` | - | Backdrop CSS classes | | `UNSTABLE_portalContainer` | `HTMLElement` | - | Custom portal container | ### Modal.Container | Prop | Type | Default | Description | | ----------- | ----------------------------------------- | ---------- | ------------------------- | | `placement` | `"auto" \| "center" \| "top" \| "bottom"` | `"auto"` | Modal position on screen | | `scroll` | `"inside" \| "outside"` | `"inside"` | Scroll behavior | | `size` | `"xs" \| "sm" \| "md" \| "lg" \| "cover" \| "full"` | `"md"` | Modal size variant | | `className` | `string \| (values) => string` | - | Container CSS classes | ### Modal.Dialog | Prop | Type | Default | Description | | ------------------ | ------------------------------------- | ---------- | -------------------------- | | `children` | `ReactNode \| ({close}) => ReactNode` | - | Content or render function | | `className` | `string \| (values) => string` | - | CSS classes | | `role` | `string` | `"dialog"` | ARIA role | | `aria-label` | `string` | - | Accessibility label | | `aria-labelledby` | `string` | - | ID of label element | | `aria-describedby` | `string` | - | ID of description element | ### Modal.Header | Prop | Type | Default | Description | | ----------- | ----------- | ------- | -------------- | | `children` | `ReactNode` | - | Header content | | `className` | `string` | - | CSS classes | ### Modal.Body | Prop | Type | Default | Description | | ----------- | ----------- | ------- | ------------ | | `children` | `ReactNode` | - | Body content | | `className` | `string` | - | CSS classes | ### Modal.Footer | Prop | Type | Default | Description | | ----------- | ----------- | ------- | -------------- | | `children` | `ReactNode` | - | Footer content | | `className` | `string` | - | CSS classes | ### Modal.CloseTrigger | Prop | Type | Default | Description | | ----------- | ------------------------------ | ------- | ------------------- | | `children` | `ReactNode` | - | Custom close button | | `className` | `string \| (values) => string` | - | CSS classes | ### useOverlayState Hook ```tsx import {useOverlayState} from "@heroui/react"; const state = useOverlayState({ defaultOpen: false, onOpenChange: (isOpen) => console.log(isOpen), }); state.isOpen; // Current state state.open(); // Open modal state.close(); // Close modal state.toggle(); // Toggle state state.setOpen(); // Set state directly ``` ## Accessibility Implements [WAI-ARIA Dialog pattern](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/): - **Focus trap**: Focus locked within modal - **Keyboard**: `ESC` closes (when enabled), `Tab` cycles elements - **Screen readers**: Proper ARIA attributes - **Scroll lock**: Body scroll disabled when open