Open UI

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.json
pnpm dlx shadcn@latest add https://oui.open.gov.sg/r/modal.json
npx shadcn@latest add https://oui.open.gov.sg/r/modal.json
bunx --bun shadcn@latest add https://oui.open.gov.sg/r/modal.json

OUI 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 isDismissable property to false to prevent the modal from closing when clicking on the overlay.
  • Set the isKeyboardDismissDisabled property to true to 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>  )}

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-center and bottom-center positions 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

PropTypeDefaultDescription
childrenReact.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
isOpenboolean-Whether the modal is open (controlled)
defaultOpenboolean-Whether the modal is open by default (uncontrolled)
isDismissablebooleantrueWhether clicking the overlay closes the modal
isKeyboardDismissDisabledbooleanfalseWhether pressing Esc closes the modal
classNamesSlotsToClasses<ModalSlots>-Custom CSS classes for component slots
onOpenChange(isOpen: boolean) => void-Callback when the open state changes

ModalContent

PropTypeDefaultDescription
childrenReactNode | ((onClose: () => void) => ReactNode)-The modal body, or a render function that receives an onClose callback
closeButtonContentReactNode-Custom content to replace the default close icon inside the close button
hideCloseButtonbooleanfalseWhether to hide the close button entirely
closeButtonPropsOmit<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&apos;s go!                  </Button>                )}              </ModalFooter>            </>          )}        </ModalContent>      </Modal>    </DialogTrigger>  )}

Draggable Modal

Use props returned by the useDraggable hook to make the modal draggable.

The zoom animation may interfere with the dragging experience (the modal will snap back to the original position on close).

It is recommended to set the animation prop to fade or none when 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 &quot;mobile&quot; size, and                    on larger screens, it uses the &quot;desktop&quot; 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>  )}

On this page