27.4k

ModalNew

Dialog overlay for focused user interactions and important content

Import

import { Modal } from '@heroui/react';

Usage

"use client";

import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";

export function Default() {
  return (
    <Modal>
      <Button variant="secondary">Open Modal</Button>
      <Modal.Container>
        <Modal.Dialog className="sm:max-w-[360px]">
          {({close}) => (
            <>
              <Modal.CloseTrigger />
              <Modal.Header>
                <Modal.Icon className="bg-default text-foreground">
                  <Icon className="size-5" icon="gravity-ui:rocket" />
                </Modal.Icon>
                <Modal.Heading>Welcome to HeroUI</Modal.Heading>
              </Modal.Header>
              <Modal.Body>
                <p>
                  A beautiful, fast, and modern React UI library for building accessible and
                  customizable web applications with ease.
                </p>
              </Modal.Body>
              <Modal.Footer>
                <Button className="w-full" onPress={close}>
                  Continue
                </Button>
              </Modal.Footer>
            </>
          )}
        </Modal.Dialog>
      </Modal.Container>
    </Modal>
  );
}

Anatomy

Import the Modal component and access all parts using dot notation.

import { Modal, Button } from '@heroui/react';

export default () => (
  <Modal>
    <Button>Open Modal</Button>
    <Modal.Container>
      <Modal.Dialog>
        <Modal.CloseTrigger />  {/* Optional: Close button */}
        <Modal.Header>
          <Modal.Icon />  {/* Optional: Icon */}
          <Modal.Heading />
        </Modal.Header>
        <Modal.Body />
        <Modal.Footer />
      </Modal.Dialog>
    </Modal.Container>
  </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 variant="secondary">
            {placement.charAt(0).toUpperCase() + placement.slice(1)}
          </Button>
          <Modal.Container placement={placement}>
            <Modal.Dialog className="sm:max-w-[360px]">
              {({close}) => (
                <>
                  <Modal.CloseTrigger />
                  <Modal.Header>
                    <Modal.Icon className="bg-default text-foreground">
                      <Icon className="size-5" icon="gravity-ui:rocket" />
                    </Modal.Icon>
                    <Modal.Heading>
                      Placement: {placement.charAt(0).toUpperCase() + placement.slice(1)}
                    </Modal.Heading>
                  </Modal.Header>
                  <Modal.Body>
                    <p>
                      This modal uses the <code>{placement}</code> placement option. Try different
                      placements to see how the modal positions itself on the screen.
                    </p>
                  </Modal.Body>
                  <Modal.Footer>
                    <Button className="w-full" onPress={close}>
                      Continue
                    </Button>
                  </Modal.Footer>
                </>
              )}
            </Modal.Dialog>
          </Modal.Container>
        </Modal>
      ))}
    </div>
  );
}

Backdrop Variants

"use client";

import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";

export function BackdropVariants() {
  const variants = ["solid", "blur", "transparent"] as const;

  return (
    <div className="flex flex-wrap gap-4">
      {variants.map((variant) => (
        <Modal key={variant}>
          <Button variant="secondary">{variant.charAt(0).toUpperCase() + variant.slice(1)}</Button>
          <Modal.Container variant={variant}>
            <Modal.Dialog className="sm:max-w-[360px]">
              {({close}) => (
                <>
                  <Modal.CloseTrigger />
                  <Modal.Header>
                    <Modal.Icon className="bg-default text-foreground">
                      <Icon className="size-5" icon="gravity-ui:rocket" />
                    </Modal.Icon>
                    <Modal.Heading>
                      Backdrop: {variant.charAt(0).toUpperCase() + variant.slice(1)}
                    </Modal.Heading>
                  </Modal.Header>
                  <Modal.Body>
                    <p>
                      This modal uses the <code>{variant}</code> backdrop variant. Compare the
                      different visual effects: solid provides full opacity, blur adds a backdrop
                      filter, and transparent removes the background.
                    </p>
                  </Modal.Body>
                  <Modal.Footer>
                    <Button className="w-full" onPress={close}>
                      Continue
                    </Button>
                  </Modal.Footer>
                </>
              )}
            </Modal.Dialog>
          </Modal.Container>
        </Modal>
      ))}
    </div>
  );
}

Dismiss Behavior

isDismissable

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

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.

"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. Defaults to{" "}
          <code>true</code>. Set to <code>false</code> to require explicit close action.
        </p>
        <Modal>
          <Button variant="secondary">Open Modal</Button>
          <Modal.Container isDismissable={false}>
            <Modal.Dialog className="sm:max-w-[360px]">
              {({close}) => (
                <>
                  <Modal.CloseTrigger />
                  <Modal.Header>
                    <Modal.Icon className="bg-default text-foreground">
                      <Icon className="size-5" icon="gravity-ui:circle-info" />
                    </Modal.Icon>
                    <Modal.Heading>isDismissable = false</Modal.Heading>
                    <p className="text-muted text-sm leading-5">
                      Clicking the backdrop won't close this modal
                    </p>
                  </Modal.Header>
                  <Modal.Body>
                    <p>
                      Try clicking outside this modal on the overlay - it won't close. You must use
                      the close button or press ESC to dismiss it.
                    </p>
                  </Modal.Body>
                  <Modal.Footer>
                    <Button className="w-full" onPress={close}>
                      Close
                    </Button>
                  </Modal.Footer>
                </>
              )}
            </Modal.Dialog>
          </Modal.Container>
        </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 users must use explicit close actions.
        </p>
        <Modal>
          <Button variant="secondary">Open Modal</Button>
          <Modal.Container isKeyboardDismissDisabled>
            <Modal.Dialog className="sm:max-w-[360px]">
              {({close}) => (
                <>
                  <Modal.CloseTrigger />
                  <Modal.Header>
                    <Modal.Icon className="bg-default text-foreground">
                      <Icon className="size-5" icon="gravity-ui:circle-info" />
                    </Modal.Icon>
                    <Modal.Heading>isKeyboardDismissDisabled = true</Modal.Heading>
                    <p className="text-muted text-sm leading-5">ESC key is disabled</p>
                  </Modal.Header>
                  <Modal.Body>
                    <p>
                      Press ESC - nothing happens. You must use the close button or click the
                      overlay backdrop to dismiss this modal.
                    </p>
                  </Modal.Body>
                  <Modal.Footer>
                    <Button className="w-full" onPress={close}>
                      Close
                    </Button>
                  </Modal.Footer>
                </>
              )}
            </Modal.Dialog>
          </Modal.Container>
        </Modal>
      </div>
    </div>
  );
}

Scroll Behavior

"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 (
    <div className="flex flex-col gap-4">
      <RadioGroup
        orientation="horizontal"
        value={scroll}
        onChange={(value) => setScroll(value as "inside" | "outside")}
      >
        <Radio value="inside">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Label>Inside</Label>
        </Radio>
        <Radio value="outside">
          <Radio.Control>
            <Radio.Indicator />
          </Radio.Control>
          <Label>Outside</Label>
        </Radio>
      </RadioGroup>

      <Modal>
        <Button variant="secondary">
          Open Modal ({scroll.charAt(0).toUpperCase() + scroll.slice(1)})
        </Button>
        <Modal.Container scroll={scroll}>
          <Modal.Dialog className="sm:max-w-[360px]">
            {({close}) => (
              <>
                <Modal.Header>
                  <Modal.Heading>
                    Scroll: {scroll.charAt(0).toUpperCase() + scroll.slice(1)}
                  </Modal.Heading>
                  <p className="text-muted text-sm leading-5">
                    Compare scroll behaviors - inside keeps content scrollable within the modal,
                    outside allows page scrolling
                  </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" onPress={close}>
                    Cancel
                  </Button>
                  <Button onPress={close}>Confirm</Button>
                </Modal.Footer>
                <Modal.CloseTrigger />
              </>
            )}
          </Modal.Dialog>
        </Modal.Container>
      </Modal>
    </div>
  );
}

With Form

"use client";

import {Button, Input, Label, Modal, Surface, TextField} from "@heroui/react";
import {Icon} from "@iconify/react";

export function WithForm() {
  return (
    <Modal>
      <Button variant="secondary">Open Contact Form</Button>
      <Modal.Container placement="auto">
        <Modal.Dialog className="sm:max-w-md">
          {({close}) => (
            <>
              <Modal.CloseTrigger />
              <Modal.Header>
                <Modal.Icon className="bg-accent-soft text-accent-soft-foreground">
                  <Icon className="size-5" icon="gravity-ui:envelope" />
                </Modal.Icon>
                <Modal.Heading>Contact Us</Modal.Heading>
                <p className="text-muted mt-1.5 text-sm leading-5">
                  Fill out the form below and we'll get back to you. The modal adapts automatically
                  when the keyboard appears on mobile.
                </p>
              </Modal.Header>
              <Modal.Body className="p-6">
                <Surface variant="default">
                  <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>
                </Surface>
              </Modal.Body>
              <Modal.Footer>
                <Button variant="secondary" onPress={close}>
                  Cancel
                </Button>
                <Button onPress={close}>Send Message</Button>
              </Modal.Footer>
            </>
          )}
        </Modal.Dialog>
      </Modal.Container>
    </Modal>
  );
}

Controlled State

With React.useState()

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

Status: closed

With useOverlayState()

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

Status: closed

"use client";

import {Button, Modal, useOverlayState} from "@heroui/react";
import {Icon} from "@iconify/react";
import {useState} from "react";

export function Controlled() {
  const [isOpen, setIsOpen] = useState(false);

  const state = useOverlayState();

  return (
    <div className="flex max-w-md flex-col gap-8">
      <div className="flex flex-col gap-3">
        <h3 className="text-foreground text-lg font-semibold">With React.useState()</h3>
        <p className="text-muted text-pretty text-sm leading-relaxed">
          Control the modal using React's <code className="text-foreground">useState</code> hook for
          simple state management. Perfect for basic use cases.
        </p>
        <div className="border-border bg-surface flex flex-col items-start gap-3 rounded-2xl border p-4 shadow-sm">
          <div className="flex w-full items-center justify-between">
            <p className="text-muted text-xs">
              Status:{" "}
              <span className="text-foreground font-mono font-medium">
                {isOpen ? "open" : "closed"}
              </span>
            </p>
          </div>
          <div className="flex gap-2">
            <Button size="sm" variant="secondary" onPress={() => setIsOpen(true)}>
              Open Modal
            </Button>
            <Button size="sm" variant="tertiary" onPress={() => setIsOpen(!isOpen)}>
              Toggle
            </Button>
          </div>
        </div>

        <Modal.Container isOpen={isOpen} onOpenChange={setIsOpen}>
          <Modal.Dialog className="sm:max-w-[360px]">
            {({close}) => (
              <>
                <Modal.CloseTrigger />
                <Modal.Header>
                  <Modal.Icon className="bg-accent-soft text-accent-soft-foreground">
                    <Icon className="size-5" icon="gravity-ui:circle-check" />
                  </Modal.Icon>
                  <Modal.Heading>Controlled with useState()</Modal.Heading>
                </Modal.Header>
                <Modal.Body>
                  <p>
                    This modal is controlled by React's <code>useState</code> hook. Pass{" "}
                    <code>isOpen</code> and <code>onOpenChange</code> props to manage the modal
                    state externally.
                  </p>
                </Modal.Body>
                <Modal.Footer>
                  <Button variant="secondary" onPress={close}>
                    Cancel
                  </Button>
                  <Button onPress={close}>Confirm</Button>
                </Modal.Footer>
              </>
            )}
          </Modal.Dialog>
        </Modal.Container>
      </div>

      <div className="flex flex-col gap-3">
        <h3 className="text-foreground text-lg font-semibold">With useOverlayState()</h3>
        <p className="text-muted text-pretty text-sm leading-relaxed">
          Use the <code className="text-foreground">useOverlayState</code> hook for a cleaner API
          with convenient methods like <code>open()</code>, <code>close()</code>, and{" "}
          <code>toggle()</code>.
        </p>
        <div className="border-border bg-surface flex flex-col items-start gap-3 rounded-2xl border p-4 shadow-sm">
          <div className="flex w-full items-center justify-between">
            <p className="text-muted text-xs">
              Status:{" "}
              <span className="text-foreground font-mono font-medium">
                {state.isOpen ? "open" : "closed"}
              </span>
            </p>
          </div>
          <div className="flex gap-2">
            <Button size="sm" variant="secondary" onPress={state.open}>
              Open Modal
            </Button>
            <Button size="sm" variant="tertiary" onPress={state.toggle}>
              Toggle
            </Button>
          </div>
        </div>

        <Modal.Container isOpen={state.isOpen} onOpenChange={state.setOpen}>
          <Modal.Dialog className="sm:max-w-[360px]">
            {({close}) => (
              <>
                <Modal.CloseTrigger />
                <Modal.Header>
                  <Modal.Icon className="bg-success-soft text-success-soft-foreground">
                    <Icon className="size-5" icon="gravity-ui:circle-check" />
                  </Modal.Icon>
                  <Modal.Heading>Controlled with useOverlayState()</Modal.Heading>
                </Modal.Header>
                <Modal.Body>
                  <p>
                    The <code>useOverlayState</code> hook provides dedicated methods for common
                    operations. No need to manually create callbacks—just use{" "}
                    <code>state.open()</code>, <code>state.close()</code>, or{" "}
                    <code>state.toggle()</code>.
                  </p>
                </Modal.Body>
                <Modal.Footer>
                  <Button variant="secondary" onPress={close}>
                    Cancel
                  </Button>
                  <Button onPress={close}>Confirm</Button>
                </Modal.Footer>
              </>
            )}
          </Modal.Dialog>
        </Modal.Container>
      </div>
    </div>
  );
}

Custom Trigger

"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-surface hover:bg-surface-secondary group flex cursor-pointer items-center gap-3 rounded-2xl border p-4 shadow-sm transition-all hover:shadow">
          <div className="bg-accent-soft text-accent-soft-foreground flex size-12 shrink-0 items-center justify-center rounded-xl transition-transform group-hover:scale-105">
            <Icon className="size-6" icon="gravity-ui:gear" />
          </div>
          <div className="flex flex-1 flex-col gap-0.5">
            <p className="text-foreground text-sm font-semibold leading-5">Settings</p>
            <p className="text-muted text-xs leading-relaxed">Manage your preferences</p>
          </div>
        </div>
      </Modal.Trigger>
      <Modal.Container>
        <Modal.Dialog className="sm:max-w-[360px]">
          {({close}) => (
            <>
              <Modal.CloseTrigger />
              <Modal.Header>
                <Modal.Icon className="bg-accent-soft text-accent-soft-foreground">
                  <Icon className="size-5" icon="gravity-ui:gear" />
                </Modal.Icon>
                <Modal.Heading>Settings</Modal.Heading>
              </Modal.Header>
              <Modal.Body>
                <p>
                  Use <code>Modal.Trigger</code> to create custom trigger elements beyond standard
                  buttons. This example shows a card-style trigger with icons and descriptive text.
                </p>
              </Modal.Body>
              <Modal.Footer>
                <Button variant="secondary" onPress={close}>
                  Cancel
                </Button>
                <Button onPress={close}>Save</Button>
              </Modal.Footer>
            </>
          )}
        </Modal.Dialog>
      </Modal.Container>
    </Modal>
  );
}

Custom Backdrop

"use client";

import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";

export function CustomBackdrop() {
  return (
    <Modal>
      <Button variant="secondary">Custom Backdrop</Button>
      <Modal.Container
        backdropClassName="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.Dialog className="sm:max-w-[360px]">
          {({close}) => (
            <>
              <Modal.Header className="items-center text-center">
                <Modal.Icon className="bg-accent-soft text-accent-soft-foreground">
                  <Icon className="size-5" icon="gravity-ui:sparkles" />
                </Modal.Icon>
                <Modal.Heading>Premium Backdrop</Modal.Heading>
              </Modal.Header>
              <Modal.Body>
                <p>
                  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.
                </p>
              </Modal.Body>
              <Modal.Footer className="flex-col-reverse">
                <Button className="w-full" onPress={close}>
                  Amazing!
                </Button>
                <Button className="w-full" variant="secondary" onPress={close}>
                  Close
                </Button>
              </Modal.Footer>
              <Modal.CloseTrigger />
            </>
          )}
        </Modal.Dialog>
      </Modal.Container>
    </Modal>
  );
}

Custom Animations

"use client";

import {Button, Modal} from "@heroui/react";
import {Icon} from "@iconify/react";

export function CustomAnimations() {
  const animations = [
    {
      classNames: [
        "data-[entering]:animate-in",
        "data-[entering]:zoom-in-95",
        "data-[entering]:fade-in-0",
        "data-[entering]:ease-[cubic-bezier(0.16,1,0.3,1)]",
        "data-[exiting]:animate-out",
        "data-[exiting]:zoom-out-95",
        "data-[exiting]:fade-out-0",
        "data-[exiting]:ease-out-quart",
      ].join(" "),
      description: "Smooth scale animation with elastic spring-like easing",
      icon: "gravity-ui:sparkles",
      name: "Smooth Scale",
    },
    {
      classNames: [
        "data-[entering]:animate-in",
        "data-[entering]:slide-in-from-bottom-4",
        "data-[entering]:fade-in-0",
        "data-[entering]:ease-fluid-out",
        "data-[exiting]:animate-out",
        "data-[exiting]:slide-out-to-bottom-2",
        "data-[exiting]:fade-out-0",
        "data-[exiting]:ease-in-quad",
      ].join(" "),
      description: "Gentle upward slide with seamless fade transition",
      icon: "gravity-ui:arrow-up-from-line",
      name: "Slide Up",
    },
  ];

  return (
    <div className="flex flex-wrap gap-4">
      {animations.map(({classNames, description, icon, name}) => (
        <Modal key={name}>
          <Button variant="secondary">{name}</Button>
          <Modal.Container
            backdropClassName="data-[exiting]:duration-250"
            className={`data-[entering]:duration-300 data-[exiting]:duration-200 ${classNames}`}
          >
            <Modal.Dialog className="sm:max-w-[360px]">
              {({close}) => (
                <>
                  <Modal.Header>
                    <Modal.Icon className="bg-default text-foreground">
                      <Icon className="size-5" icon={icon} />
                    </Modal.Icon>
                    <Modal.Heading>{name} Animation</Modal.Heading>
                  </Modal.Header>
                  <Modal.Body>
                    <p className="mt-1">
                      {description}. Customize entrance and exit animations using Tailwind's
                      animation utilities. Combine <code>data-[entering]</code> and{" "}
                      <code>data-[exiting]</code> states with custom timings and easing functions
                      for polished transitions.
                    </p>
                  </Modal.Body>
                  <Modal.Footer>
                    <Button variant="tertiary" onPress={close}>
                      Close
                    </Button>
                    <Button onPress={close}>Try Again</Button>
                  </Modal.Footer>
                </>
              )}
            </Modal.Dialog>
          </Modal.Container>
        </Modal>
      ))}
    </div>
  );
}

Styling

Passing Tailwind CSS classes

import { Modal, Button } from '@heroui/react';

function CustomModal() {
  return (
    <Modal>
      <Button>Open Modal</Button>
      <Modal.Container
        backdropClassName="bg-black/80"
        className="items-start pt-20"
      >
        <Modal.Dialog className="bg-gradient-to-br from-purple-500 to-pink-500 text-white">
          <Modal.Header>
            <h2>Custom Styled Modal</h2>
          </Modal.Header>
          <Modal.Body>
            <p>This modal has custom styling applied via Tailwind classes</p>
          </Modal.Body>
          <Modal.Footer>
            <Button>Close</Button>
          </Modal.Footer>
        </Modal.Dialog>
      </Modal.Container>
    </Modal>
  );
}

Customizing the component classes

To customize the Modal component classes, you can use the @layer components directive.
Learn more.

@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 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):

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--solid - Solid 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

PropTypeDefaultDescription
childrenReactNode-Trigger and container elements

Modal.Trigger

PropTypeDefaultDescription
childrenReactNode-Custom trigger content
classNamestring-CSS classes

Modal.Container

PropTypeDefaultDescription
placement"auto" | "center" | "top" | "bottom""auto"Modal position on screen
scroll"inside" | "outside""inside"Scroll behavior
variant"solid" | "blur" | "transparent""solid"Backdrop overlay style
isDismissablebooleantrueClose on backdrop click
isKeyboardDismissDisabledbooleanfalseDisable ESC key to close
isOpenboolean-Controlled open state
onOpenChange(isOpen: boolean) => void-Open state change handler
backdropClassNamestring | (values) => string-Backdrop CSS classes
classNamestring | (values) => string-Container CSS classes

Modal.Dialog

PropTypeDefaultDescription
childrenReactNode | ({close}) => ReactNode-Content or render function
classNamestring | (values) => string-CSS classes
rolestring"dialog"ARIA role
aria-labelstring-Accessibility label
aria-labelledbystring-ID of label element
aria-describedbystring-ID of description element

Modal.Header

PropTypeDefaultDescription
childrenReactNode-Header content
classNamestring-CSS classes

Modal.Body

PropTypeDefaultDescription
childrenReactNode-Body content
classNamestring-CSS classes

Modal.Footer

PropTypeDefaultDescription
childrenReactNode-Footer content
classNamestring-CSS classes

Modal.CloseTrigger

PropTypeDefaultDescription
asChildbooleanfalseRender as child
childrenReactNode-Custom close button
classNamestring | (values) => string-CSS classes

useOverlayState Hook

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:

  • 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