Modal
A modal is an overlay element which blocks interaction with elements outside it.
import { DialogTrigger } from "react-aria-components"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"export const Example = () => { return ( <DialogTrigger> <Button>Open Modal</Button> <Modal> <ModalContent> {(onClose) => ( <> <ModalHeader>Modal Title</ModalHeader> <ModalBody> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. </p> </ModalBody> <ModalFooter> <Button color="critical" variant="reverse" onPress={onClose}> Close </Button> <Button color="main" onPress={onClose}> Action </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> )}Usage
Use Modal for blocking interactions that require explicit user action or attention. Use Tooltip for non-blocking hints. Use Menu for action lists.
import {
Button,
Modal,
ModalBody,
ModalContent,
ModalFooter,
ModalHeader,
} from "@opengovsg/oui"<DialogTrigger>
<Button>Open Modal</Button>
<Modal>
<ModalContent>
{(onClose) => (
<>
<ModalHeader>Modal Title</ModalHeader>
<ModalBody>
<p>Your modal content here</p>
</ModalBody>
<ModalFooter>
<Button onPress={onClose}>Close</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
</DialogTrigger>Alternatively, install the component as local source via the shadcn CLI:
npx shadcn@latest add https://oui.open.gov.sg/r/modal.jsonpnpm dlx shadcn@latest add https://oui.open.gov.sg/r/modal.jsonnpx shadcn@latest add https://oui.open.gov.sg/r/modal.jsonbunx --bun shadcn@latest add https://oui.open.gov.sg/r/modal.jsonOUI exports 5 modal-related components:
- Modal: The main component to display a modal.
- ModalContent: The wrapper of the other modal components.
- ModalHeader: The header of the modal.
- ModalBody: The body of the modal.
- ModalFooter: The footer of the modal.
When the modal opens:
- Focus is bounded within the modal and set to the first tabbable element.
- Content behind the modal dialog is inert, meaning that users cannot interact with it.
Examples
Sizes
Use the size prop to change the size of the modal. You will mainly use the desktop, mobile, and full sizes if following Figma.
However, you can also choose from other preset sizes such as xs, sm, md, lg, xl, 2xl, 3xl, 4xl, or 5xl depending on your use case.
import { DialogTrigger } from "react-aria-components"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"export const Example = () => { const mainSizes = ["desktop", "mobile", "full"] as const const extraSizes = [ "xs", "sm", "md", "lg", "xl", "2xl", "3xl", "4xl", "5xl", ] as const return ( <div className="flex flex-col gap-4"> <div className="flex flex-wrap gap-2"> {mainSizes.map((size) => ( <DialogTrigger key={size}> <Button variant="outline">{size}</Button> <Modal size={size}> <ModalContent> {(onClose) => ( <> <ModalHeader>Modal Size: {size}</ModalHeader> <ModalBody> <p> This modal demonstrates the <strong>{size}</strong> size variant. </p> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. </p> </ModalBody> <ModalFooter> <Button color="critical" variant="reverse" onPress={onClose} > Close </Button> <Button color="main" onPress={onClose}> Action </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> ))} </div> <div className="flex flex-wrap gap-2"> {extraSizes.map((size) => ( <DialogTrigger key={size}> <Button variant="outline">{size}</Button> <Modal size={size}> <ModalContent> {(onClose) => ( <> <ModalHeader>Modal Size: {size}</ModalHeader> <ModalBody> <p> This modal demonstrates the <strong>{size}</strong> size variant. </p> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. </p> </ModalBody> <ModalFooter> <Button color="critical" variant="reverse" onPress={onClose} > Close </Button> <Button color="main" onPress={onClose}> Action </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> ))} </div> </div> )}Non-dismissible
By default, the modal can be closed by clicking on the overlay or pressing the Esc key. You can disable this behavior by setting the following properties:
- Set the
isDismissableproperty tofalseto prevent the modal from closing when clicking on the overlay. - Set the
isKeyboardDismissDisabledproperty totrueto prevent the modal from closing when pressing the Esc key.
import { DialogTrigger } from "react-aria-components"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"export const Example = () => { return ( <DialogTrigger> <Button>Open Non-dismissible Modal</Button> <Modal isDismissable={false} isKeyboardDismissDisabled> <ModalContent> {(onClose) => ( <> <ModalHeader>Non-dismissible Modal</ModalHeader> <ModalBody> <p> This modal cannot be closed by clicking the overlay or pressing the Escape key. You must use the button below to close it. </p> </ModalBody> <ModalFooter> <Button color="main" onPress={onClose}> I understand </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> )}Modal Placement
By default, the modal is centered on screens larger than sm and is at the bottom of the screen on mobile. This placement is called auto, but you can change it by using the placement prop.
Note: The
top-centerandbottom-centerpositions mean that the modal is positioned at the top/bottom of the screen on mobile, and at the center of the screen on desktop.
import { useState } from "react"import { DialogTrigger } from "react-aria-components"import type { ModalProps } from "@opengovsg/oui"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"export const Example = () => { const [modalPlacement, setModalPlacement] = useState<ModalProps["placement"]>("auto") const placements: ModalProps["placement"][] = [ "auto", "top", "bottom", "center", "top-center", "bottom-center", ] return ( <div className="flex flex-col gap-4 p-4 md:p-6 lg:p-10"> <div className="flex flex-wrap gap-2"> {placements.map((placement) => ( <Button key={placement} size="sm" variant={modalPlacement === placement ? "solid" : "outline"} onPress={() => setModalPlacement(placement)} > {placement} </Button> ))} </div> <DialogTrigger> <Button>Open Modal</Button> <Modal placement={modalPlacement}> <ModalContent> {(onClose) => ( <> <div className="flex h-[160px] items-center justify-center bg-blue-100"> <p>Some picture here</p> </div> <ModalHeader className="flex flex-col gap-1"> Modal Title </ModalHeader> <ModalBody> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. </p> </ModalBody> <ModalFooter> <Button color="neutral" variant="clear" onPress={onClose}> Maybe later </Button> <Button color="main" onPress={onClose}> Learn more </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> </div> )}Overflow Scroll
You can use the scrollBehavior prop to set the scroll behavior of the modal.
- normal: The modal content will not be scrollable.
- inside: The modal content will be scrollable.
- outside: The modal content will be scrollable and the modal will be fixed.
The default value is normal.
import { DialogTrigger } from "react-aria-components"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"export const Example = () => { const scrollBehaviors = ["normal", "inside", "outside"] as const return ( <div className="flex flex-wrap gap-2"> {scrollBehaviors.map((behavior) => ( <DialogTrigger key={behavior}> <Button variant="outline">{behavior}</Button> <Modal scrollBehavior={behavior}> <ModalContent> {(onClose) => ( <> <ModalHeader>Scroll Behavior: {behavior}</ModalHeader> <ModalBody> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. </p> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. </p> <p> Magna exercitation reprehenderit magna aute tempor cupidatat consequat elit dolor adipisicing. Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. </p> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. Magna exercitation reprehenderit magna aute tempor cupidatat consequat elit dolor adipisicing. Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. </p> <p> Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. Magna exercitation reprehenderit magna aute tempor cupidatat consequat elit dolor adipisicing. Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. </p> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. </p> <p> Magna exercitation reprehenderit magna aute tempor cupidatat consequat elit dolor adipisicing. Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. </p> <p> Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. Magna exercitation reprehenderit magna aute tempor cupidatat consequat elit dolor adipisicing. Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. </p> <p> Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. Magna exercitation reprehenderit magna aute tempor cupidatat consequat elit dolor adipisicing. Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. </p> <p> Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. Magna exercitation reprehenderit magna aute tempor cupidatat consequat elit dolor adipisicing. Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. </p> <p> Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam. Magna exercitation reprehenderit magna aute tempor cupidatat consequat elit dolor adipisicing. Mollit dolor eiusmod sunt ex incididunt cillum quis. Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam. </p> </ModalBody> <ModalFooter> <Button color="critical" variant="reverse" onPress={onClose} > Close </Button> <Button color="main" onPress={onClose}> Action </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> ))} </div> )}Backdrop
The Modal component has an overlay prop to show a backdrop behind the modal. The backdrop can be either transparent, opaque or blur. The default value is blur.
import { DialogTrigger } from "react-aria-components"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"export const Example = () => { const overlays = ["blur", "opaque", "transparent"] as const return ( <div className="flex flex-wrap gap-2"> {overlays.map((overlay) => ( <DialogTrigger key={overlay}> <Button variant="outline">{overlay}</Button> <Modal overlay={overlay}> <ModalContent> {(onClose) => ( <> <ModalHeader>Backdrop: {overlay}</ModalHeader> <ModalBody> <p> This modal uses the <strong>{overlay}</strong> backdrop style. </p> </ModalBody> <ModalFooter> <Button color="critical" variant="reverse" onPress={onClose} > Close </Button> <Button color="main" onPress={onClose}> Action </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> ))} </div> )}Custom Backdrop
You can customize the backdrop by using the overlay slot in classNames.
import { DialogTrigger } from "react-aria-components"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"export const Example = () => { return ( <DialogTrigger> <Button>Open Modal with Custom Backdrop</Button> <Modal classNames={{ overlay: "bg-gradient-to-br from-purple-500/50 to-pink-500/50", }} > <ModalContent> {(onClose) => ( <> <ModalHeader>Custom Backdrop</ModalHeader> <ModalBody> <p> This modal has a custom gradient backdrop applied using the classNames prop. </p> </ModalBody> <ModalFooter> <Button color="critical" variant="reverse" onPress={onClose}> Close </Button> <Button color="main" onPress={onClose}> Action </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> )}Controlled
You can control the modal's open state using the isOpen and onOpenChange props.
import { useState } from "react"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"export const Example = () => { const [isOpen, setIsOpen] = useState(false) return ( <> <Button onPress={() => setIsOpen(true)}>Open Controlled Modal</Button> <Modal isOpen={isOpen} onOpenChange={setIsOpen}> <ModalContent> {(onClose) => ( <> <ModalHeader>Controlled Modal</ModalHeader> <ModalBody> <p> This modal is controlled programmatically using the{" "} <code>isOpen</code> and <code>onOpenChange</code> props. </p> <p>Current state: {isOpen ? "Open" : "Closed"}</p> </ModalBody> <ModalFooter> <Button color="critical" variant="reverse" onPress={onClose}> Close </Button> <Button color="main" onPress={onClose}> Action </Button> </ModalFooter> </> )} </ModalContent> </Modal> </> )}Animation
Use the animation prop to control the opening and closing animation of the modal.
- zoom: The modal scales in and out (default).
- fade: The modal fades in and out.
- none: No animation.
import { DialogTrigger } from "react-aria-components"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"const ANIMATIONS = [ { value: "zoom", label: "Zoom (default)", description: "the default zoom" }, { value: "fade", label: "Fade", description: "the fade" }, { value: "none", label: "None", description: "no" },] as constexport const Example = () => { return ( <div className="flex flex-wrap gap-3"> {ANIMATIONS.map(({ value, label, description }) => ( <DialogTrigger key={value}> <Button variant="outline">{label}</Button> <Modal animation={value}> <ModalContent> {(onClose) => ( <> <ModalHeader>{label} Animation</ModalHeader> <ModalBody> <p>This modal uses {description} animation.</p> </ModalBody> <ModalFooter> <Button onPress={onClose}>Close</Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> ))} </div> )}Accessibility
- Content outside the modal is hidden from assistive technologies while it is open.
- The modal optionally closes when interacting outside, or pressing the Esc key.
- Focus is moved into the modal on mount, and restored to the trigger element on unmount.
- While open, focus is contained within the modal, preventing the user from tabbing outside.
- Scrolling the page behind the modal is prevented while it is open, including in mobile browsers.
Slots
Slots are named regions of the component you can target with custom Tailwind classes via the classNames prop. Each slot below corresponds to a key on the classNames object.
overlay: The backdrop/overlay element rendered behind the modal.base: The main slot of the modal content.dialog: The dialog wrapper inside the modal.header: The header section rendered at the top of the modal.body: The body section rendered in the middle of the modal.footer: The footer section rendered at the bottom of the modal.closeButton: The close button rendered in the top corner of the modal.
Custom Styles
You can customize the Modal component by passing custom Tailwind CSS classes to the component slots via the classNames prop.
<Modal
classNames={{
base: "bg-white",
overlay: "bg-black/50",
header: "border-b",
body: "py-6",
footer: "border-t",
}}
>
{/* ... */}
</Modal>Props
Modal
| Prop | Type | Default | Description |
|---|---|---|---|
children | React.ReactNode | - | The content of the modal |
size | "xs" | "sm" | "md" | "lg" | "xl" | "2xl" | "3xl" | "4xl" | "5xl" | "desktop" | "mobile" | "full" | "desktop" | The size of the modal |
radius | "none" | "sm" | "md" | "lg" | "sm" | The border radius of the modal |
overlay | "transparent" | "opaque" | "blur" | "blur" | The backdrop style |
animation | "none" | "fade" | "zoom" | "zoom" | The animation style for opening and closing |
scrollBehavior | "normal" | "inside" | "outside" | "normal" | The scroll behavior of the modal |
placement | "auto" | "center" | "top" | "top-center" | "bottom" | "bottom-center" | "auto" | The placement of the modal |
isOpen | boolean | - | Whether the modal is open (controlled) |
defaultOpen | boolean | - | Whether the modal is open by default (uncontrolled) |
isDismissable | boolean | true | Whether clicking the overlay closes the modal |
isKeyboardDismissDisabled | boolean | false | Whether pressing Esc closes the modal |
classNames | SlotsToClasses<ModalSlots> | - | Custom CSS classes for component slots |
onOpenChange | (isOpen: boolean) => void | - | Callback when the open state changes |
ModalContent
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((onClose: () => void) => ReactNode) | - | The modal body, or a render function that receives an onClose callback |
closeButtonContent | ReactNode | - | Custom content to replace the default close icon inside the close button |
hideCloseButton | boolean | false | Whether to hide the close button entirely |
closeButtonProps | Omit<ButtonProps, "className" | "slot"> | - | Additional props spread onto the close button element |
Inherits all remaining props from React Aria's Dialog. Notable ones: aria-label, aria-labelledby, aria-describedby.
ModalHeader
Accepts all HeadingProps from React Aria (level, className, children, etc.).
ModalBody
Accepts all HTML <div> attributes (className, children, etc.).
ModalFooter
Accepts all HTML <div> attributes (className, children, etc.).
Examples
Feature Announcement
import { useState } from "react"import { DialogTrigger } from "react-aria-components"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"export const Example = () => { const CONTENT = [ { header: "Feature header 1", body: `Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. Pellentesque sit amet hendrerit risus, sed porttitor quam.`, }, { header: "Feature header 2", body: `Magna exercitation reprehenderit magna aute tempor cupidatat consequat elit dolor adipisicing. Mollit dolor eiusmod sunt ex incididunt cillum quis.`, }, { header: "Feature header 3", body: `Velit duis sit officia eiusmod Lorem aliqua enim laboris do dolor eiusmod. Et mollit incididunt nisi consectetur esse laborum eiusmod pariatur proident Lorem eiusmod et. Culpa deserunt nostrud ad veniam.`, }, ] const [step, setStep] = useState(0) const prev = () => { setStep((prev) => Math.max(prev - 1, 0)) } const next = () => { setStep((prev) => Math.min(prev + 1, CONTENT.length - 1)) } const hasNext = step < CONTENT.length - 1 const hasPrev = step > 0 return ( <DialogTrigger> <Button>Open Modal</Button> <Modal> <ModalContent> {(onClose) => ( <> <div className="flex h-[160px] items-center justify-center bg-blue-100"> <p>Some picture here</p> </div> <ModalHeader className="flex flex-col gap-1"> {CONTENT[step].header} </ModalHeader> <ModalBody> <p>{CONTENT[step].body}</p> </ModalBody> <ModalFooter> <Button color="neutral" variant="clear" onPress={prev} isDisabled={!hasPrev} > Back </Button> {hasNext && ( <Button color="main" onPress={next} isDisabled={!hasNext}> Next </Button> )} {!hasNext && ( <Button color="main" onPress={onClose}> Let's go! </Button> )} </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> )}Draggable Modal
Use props returned by the useDraggable hook to make the modal draggable.
The
zoomanimation may interfere with the dragging experience (the modal will snap back to the original position on close).It is recommended to set the
animationprop tofadeornonewhen using a draggable modal.
import { useRef, useState } from "react"import { DialogTrigger } from "react-aria-components"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, useDraggable,} from "@opengovsg/oui"export const Example = () => { const [isOpen, onOpen] = useState(false) const targetRef = useRef<HTMLDivElement>(null) const { moveProps } = useDraggable({ targetRef, isDisabled: !isOpen }) return ( <DialogTrigger isOpen={isOpen} onOpenChange={onOpen}> <Button>Open Draggable Modal</Button> <Modal animation="fade" ref={targetRef}> <ModalContent> {(onClose) => ( <> <ModalHeader {...moveProps}>Draggable Modal</ModalHeader> <ModalBody> <p> Drag the modal header to move this modal around the screen. </p> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. </p> </ModalBody> <ModalFooter> <Button color="critical" variant="reverse" onPress={onClose}> Close </Button> <Button color="main" onPress={onClose}> Action </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> )}Draggable Overflow Modal
Setting useDraggable#overflow prop to true allows users to drag the modal to a position where it overflows the viewport.
import { useRef, useState } from "react"import { DialogTrigger } from "react-aria-components"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, useDraggable,} from "@opengovsg/oui"export const Example = () => { const [isOpen, onOpen] = useState(false) const targetRef = useRef<HTMLDivElement>(null) const { moveProps } = useDraggable({ targetRef, isDisabled: !isOpen, // 👇 Set true to enable dragging beyond viewport canOverflow: true, }) return ( <DialogTrigger isOpen={isOpen} onOpenChange={onOpen}> <Button>Open Draggable Overflow Modal</Button> <Modal animation="fade" ref={targetRef}> <ModalContent> {(onClose) => ( <> <ModalHeader {...moveProps}>Draggable Modal</ModalHeader> <ModalBody> <p> Drag the modal header to move this modal around the screen. </p> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam pulvinar risus non risus hendrerit venenatis. </p> </ModalBody> <ModalFooter> <Button color="critical" variant="reverse" onPress={onClose}> Close </Button> <Button color="main" onPress={onClose}> Action </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> )}Responsive Modal
You can use the useMediaQuery hook from usehooks-ts (or other similar libraries) to create a responsive modal that changes its size and placement based on the screen size.
import { DialogTrigger } from "react-aria-components"import { useMediaQuery } from "usehooks-ts"import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader,} from "@opengovsg/oui"export const Example = () => { const isMobile = useMediaQuery("(max-width: 640px)") return ( <div className="flex flex-col gap-4 p-4 md:p-6 lg:p-10"> <DialogTrigger> <Button>Open Responsive Modal</Button> <Modal size={isMobile ? "mobile" : "desktop"} placement="bottom-center"> <ModalContent> {(onClose) => ( <> <ModalHeader>Responsive Modal</ModalHeader> <ModalBody> <p> This modal automatically adjusts its size based on the screen width. </p> <p> On mobile devices, it uses the "mobile" size, and on larger screens, it uses the "desktop" size. </p> <p className="text-muted-foreground mt-2 text-sm"> Current mode:{" "} <span className="font-medium"> {isMobile ? "Mobile" : "Desktop"} </span> </p> </ModalBody> <ModalFooter> <Button color="critical" variant="reverse" onPress={onClose}> Close </Button> <Button color="main" onPress={onClose}> Action </Button> </ModalFooter> </> )} </ModalContent> </Modal> </DialogTrigger> </div> )}