27.5k

Modal

Migration guide for Modal from HeroUI v2 to v3

Refer to the v3 Modal documentation for complete API reference, styling guide, and advanced examples. This guide only focuses on migrating from HeroUI v2.

Overview

The Modal component in HeroUI v3 has been redesigned with a compound component pattern, requiring explicit structure with Modal.Container, Modal.Dialog, Modal.Header, Modal.Body, and Modal.Footer components.

Structure Changes

v2: Separate Components

In v2, Modal used separate components:

import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, useDisclosure } from "@heroui/react";

const {isOpen, onOpen, onOpenChange} = useDisclosure();

<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
  <ModalContent>
    <ModalHeader>Title</ModalHeader>
    <ModalBody>Content</ModalBody>
    <ModalFooter>Footer</ModalFooter>
  </ModalContent>
</Modal>

v3: Compound Components

In v3, Modal uses compound components:

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

<Modal>
  <Button>Open Modal</Button>
  <Modal.Container>
    <Modal.Dialog>
      <Modal.CloseTrigger />
      <Modal.Header>
        <Modal.Heading>Title</Modal.Heading>
      </Modal.Header>
      <Modal.Body>Content</Modal.Body>
      <Modal.Footer>Footer</Modal.Footer>
    </Modal.Dialog>
  </Modal.Container>
</Modal>

Key Changes

1. Component Structure

v2: Separate components (Modal, ModalContent, ModalHeader, ModalBody, ModalFooter)
v3: Compound components (Modal.Container, Modal.Dialog, Modal.Header, Modal.Body, Modal.Footer)

2. State Management

v2: Uses useDisclosure hook
v3: Built-in trigger or controlled with isOpen/onOpenChange on Modal.Container

3. Prop Changes

v2 Propv3 PropNotes
size-Removed (use Tailwind CSS)
radius-Removed (use Tailwind CSS)
shadow-Removed (use Tailwind CSS)
backdropvariant (on Container)Renamed (opaquesolid)
scrollBehaviorscroll (on Container)Renamed (normalinside)
placementplacement (on Container)Moved to Container
isDismissableisDismissable (on Container)Moved to Container
isKeyboardDismissDisabledisKeyboardDismissDisabled (on Container)Moved to Container
isOpenisOpen (on Container)Moved to Container
onOpenChangeonOpenChange (on Container)Moved to Container
onClose-Use close from render prop
hideCloseButton-Omit Modal.CloseTrigger instead
closeButton-Use Modal.CloseTrigger with custom content
motionProps-Removed (animations handled differently)
classNames-Use className props
shouldBlockScroll-Removed (handled automatically)
portalContainer-Removed

4. Removed Props

The following props are no longer available in v3:

  • size - Use Tailwind CSS classes (e.g., sm:max-w-[360px])
  • radius - Use Tailwind CSS classes (e.g., rounded-lg)
  • shadow - Use Tailwind CSS classes (e.g., shadow-lg)
  • motionProps - Animations handled differently
  • classNames - Use className props on individual components
  • shouldBlockScroll - Handled automatically
  • portalContainer - Portal handling changed
  • hideCloseButton - Omit Modal.CloseTrigger instead
  • closeButton - Use Modal.CloseTrigger with custom content

Migration Examples

Basic Usage

import {
  Modal,
  ModalContent,
  ModalHeader,
  ModalBody,
  ModalFooter,
  Button,
  useDisclosure,
} from "@heroui/react";

export default function App() {
  const {isOpen, onOpen, onOpenChange} = useDisclosure();

  return (
    <>
      <Button onPress={onOpen}>Open Modal</Button>
      <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
        <ModalContent>
          {(onClose) => (
            <>
              <ModalHeader>Modal Title</ModalHeader>
              <ModalBody>
                <p>Modal content goes here</p>
              </ModalBody>
              <ModalFooter>
                <Button onPress={onClose}>Close</Button>
              </ModalFooter>
            </>
          )}
        </ModalContent>
      </Modal>
    </>
  );
}
import { Modal, Button } from "@heroui/react";

export default function App() {
  return (
    <Modal>
      <Button>Open Modal</Button>
      <Modal.Container>
        <Modal.Dialog className="sm:max-w-[360px]">
          {({close}) => (
            <>
              <Modal.CloseTrigger />
              <Modal.Header>
                <Modal.Heading>Modal Title</Modal.Heading>
              </Modal.Header>
              <Modal.Body>
                <p>Modal content goes here</p>
              </Modal.Body>
              <Modal.Footer>
                <Button onPress={close}>Close</Button>
              </Modal.Footer>
            </>
          )}
        </Modal.Dialog>
      </Modal.Container>
    </Modal>
  );
}

Controlled Modal

import { useDisclosure } from "@heroui/react";

const {isOpen, onOpen, onOpenChange} = useDisclosure();

<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
  <ModalContent>
    {(onClose) => (
      <>
        <ModalHeader>Title</ModalHeader>
        <ModalBody>Content</ModalBody>
      </>
    )}
  </ModalContent>
</Modal>
import { useState } from "react";

const [isOpen, setIsOpen] = useState(false);

<Modal>
  <Button onPress={() => setIsOpen(true)}>Open</Button>
  <Modal.Container isOpen={isOpen} onOpenChange={setIsOpen}>
    <Modal.Dialog>
      {({close}) => (
        <>
          <Modal.Header>
            <Modal.Heading>Title</Modal.Heading>
          </Modal.Header>
          <Modal.Body>Content</Modal.Body>
        </>
      )}
    </Modal.Dialog>
  </Modal.Container>
</Modal>

With Sizes

<Modal size="lg">
  <ModalContent>
    {/* content */}
  </ModalContent>
</Modal>
<Modal.Container>
  <Modal.Dialog className="sm:max-w-[640px]">
    {/* content */}
  </Modal.Dialog>
</Modal.Container>

With Backdrop

<Modal backdrop="blur">
  <ModalContent>
    {/* content */}
  </ModalContent>
</Modal>
<Modal.Container variant="blur">
  <Modal.Dialog>
    {/* content */}
  </Modal.Dialog>
</Modal.Container>

With Placement

<Modal placement="top">
  <ModalContent>
    {/* content */}
  </ModalContent>
</Modal>
<Modal.Container placement="top">
  <Modal.Dialog>
    {/* content */}
  </Modal.Dialog>
</Modal.Container>

With Scroll Behavior

<Modal scrollBehavior="outside">
  <ModalContent>
    {/* content */}
  </ModalContent>
</Modal>
<Modal.Container scroll="outside">
  <Modal.Dialog>
    {/* content */}
  </Modal.Dialog>
</Modal.Container>

Without Close Button

<Modal hideCloseButton>
  <ModalContent>
    {/* content */}
  </ModalContent>
</Modal>
<Modal.Container>
  <Modal.Dialog>
    {/* Omit Modal.CloseTrigger */}
    <Modal.Header>
      <Modal.Heading>Title</Modal.Heading>
    </Modal.Header>
  </Modal.Dialog>
</Modal.Container>

Custom Close Button

<Modal closeButton={<CustomCloseIcon />}>
  <ModalContent>
    {/* content */}
  </ModalContent>
</Modal>
<Modal.Container>
  <Modal.Dialog>
    <Modal.CloseTrigger>
      <CustomCloseIcon />
    </Modal.CloseTrigger>
    {/* content */}
  </Modal.Dialog>
</Modal.Container>

With Icon and Heading

<ModalHeader className="flex flex-col gap-1">
  <Icon />
  Modal Title
</ModalHeader>
<Modal.Header>
  <Modal.Icon>
    <Icon />
  </Modal.Icon>
  <Modal.Heading>Modal Title</Modal.Heading>
</Modal.Header>

Component Anatomy

The v3 Modal follows this structure:

Modal (Root)
  ├── Modal.Trigger (optional, or use Button)
  └── Modal.Container
      └── Modal.Dialog
          ├── Modal.CloseTrigger (optional)
          ├── Modal.Header
          │   ├── Modal.Icon (optional)
          │   └── Modal.Heading
          ├── Modal.Body
          └── Modal.Footer

Breaking Changes Summary

  1. Component Structure: Must use compound components (Modal.Container, Modal.Dialog, etc.)
  2. State Management: Built-in trigger or controlled state on Modal.Container
  3. Props Moved: Many props moved from Modal to Modal.Container
  4. Close Handler: onClose callback replaced with close render prop
  5. Close Button: hideCloseButton/closeButton replaced with Modal.CloseTrigger
  6. Styling Props Removed: size, radius, shadow - use Tailwind CSS
  7. Backdrop Renamed: backdropvariant (opaquesolid)
  8. Scroll Renamed: scrollBehaviorscroll (normalinside)
  9. Motion Removed: motionProps removed, animations handled differently
  10. New Components: Modal.Icon, Modal.Heading, Modal.Trigger

Tips for Migration

  1. Update structure: Wrap content in Modal.Container and Modal.Dialog
  2. Move props: Move placement, backdrop, scrollBehavior, etc. to Modal.Container
  3. Update state: Use built-in trigger or controlled state on Modal.Container
  4. Replace close: Use close from render prop instead of onClose callback
  5. Add close trigger: Include Modal.CloseTrigger or omit it to hide
  6. Update styling: Use Tailwind CSS classes for sizes, radius, shadows
  7. Add heading: Use Modal.Heading inside Modal.Header
  8. Add icon: Use Modal.Icon inside Modal.Header if needed

Need Help?

For v3 Modal features and API:

For community support: