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 Prop | v3 Prop | Notes |
|---|---|---|
size | - | Removed (use Tailwind CSS) |
radius | - | Removed (use Tailwind CSS) |
shadow | - | Removed (use Tailwind CSS) |
backdrop | variant (on Container) | Renamed (opaque → solid) |
scrollBehavior | scroll (on Container) | Renamed (normal → inside) |
placement | placement (on Container) | Moved to Container |
isDismissable | isDismissable (on Container) | Moved to Container |
isKeyboardDismissDisabled | isKeyboardDismissDisabled (on Container) | Moved to Container |
isOpen | isOpen (on Container) | Moved to Container |
onOpenChange | onOpenChange (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 differentlyclassNames- UseclassNameprops on individual componentsshouldBlockScroll- Handled automaticallyportalContainer- Portal handling changedhideCloseButton- OmitModal.CloseTriggerinsteadcloseButton- UseModal.CloseTriggerwith 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.FooterBreaking Changes Summary
- Component Structure: Must use compound components (
Modal.Container,Modal.Dialog, etc.) - State Management: Built-in trigger or controlled state on
Modal.Container - Props Moved: Many props moved from
ModaltoModal.Container - Close Handler:
onClosecallback replaced withcloserender prop - Close Button:
hideCloseButton/closeButtonreplaced withModal.CloseTrigger - Styling Props Removed:
size,radius,shadow- use Tailwind CSS - Backdrop Renamed:
backdrop→variant(opaque→solid) - Scroll Renamed:
scrollBehavior→scroll(normal→inside) - Motion Removed:
motionPropsremoved, animations handled differently - New Components:
Modal.Icon,Modal.Heading,Modal.Trigger
Tips for Migration
- Update structure: Wrap content in
Modal.ContainerandModal.Dialog - Move props: Move
placement,backdrop,scrollBehavior, etc. toModal.Container - Update state: Use built-in trigger or controlled state on
Modal.Container - Replace close: Use
closefrom render prop instead ofonClosecallback - Add close trigger: Include
Modal.CloseTriggeror omit it to hide - Update styling: Use Tailwind CSS classes for sizes, radius, shadows
- Add heading: Use
Modal.HeadinginsideModal.Header - Add icon: Use
Modal.IconinsideModal.Headerif needed
Need Help?
For v3 Modal features and API:
- See the API Reference
- Check interactive examples
For community support: