Skip to main contentSkip to docs navigation

Renders a popup dialog with header, body, and footer sections, configurable sizing, scrollable content, and an optional fullscreen mode.

Examples

<Modal> composes <ModalHeader>, <ModalBody>, and <ModalFooter> sub-components around its dialog content.

This static example is rendered with open, so it is open from the first paint as a non-modal dialog, and disables the backdrop and keyboard dismissal so it stays open for illustration. It includes a modal header, a modal body (required for padding), and an optional modal footer. Include a modal header with a dismiss action whenever possible, or provide another explicit dismiss action.

JSX
<Modal backdrop={false} keyboard={false} open>
  <ModalHeader>
    <ModalTitle>Modal title</ModalTitle>
  </ModalHeader>
  <ModalBody>Modal body text goes here.</ModalBody>
  <ModalFooter>
    <Button color="secondary">Close</Button>
    <Button color="primary">Save changes</Button>
  </ModalFooter>
</Modal>

Live demo

Toggling the button below opens a working modal that slides down and fades in from the top of the page.

visible opens and closes the modal. A close request (the Escape key, a backdrop click, a close button) fires onClose, and onVisibleChange(false) beside it, so visible={open} onVisibleChange={setOpen} works as well as onClose={() => setOpen(false)}. With defaultVisible instead, the modal is open at first and closes itself.

JSX
import { useState } from 'react'
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle } from '@chassis-ui/react'

export const Example = () => {
  const [visible, setVisible] = useState(false)
  return (
    <>
      <Button onClick={() => setVisible(!visible)}>Launch demo modal</Button>
      <Modal visible={visible} onClose={() => setVisible(false)}>
        <ModalHeader>
          <ModalTitle>Modal title</ModalTitle>
        </ModalHeader>
        <ModalBody>Woohoo, you're reading this text in a modal!</ModalBody>
        <ModalFooter>
          <Button color="secondary" onClick={() => setVisible(false)}>
            Close
          </Button>
          <Button color="primary">Save changes</Button>
        </ModalFooter>
      </Modal>
    </>
  )
}

Static backdrop

Set backdrop to "static" to prevent the modal from closing when the backdrop is clicked.

JSX
import { useState } from 'react'
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle } from '@chassis-ui/react'

export const Example = () => {
  const [visible, setVisible] = useState(false)
  return (
    <>
      <Button onClick={() => setVisible(!visible)}>Launch static backdrop modal</Button>
      <Modal backdrop="static" visible={visible} onClose={() => setVisible(false)}>
        <ModalHeader>
          <ModalTitle>Modal title</ModalTitle>
        </ModalHeader>
        <ModalBody>
          I will not close if you click outside me. Don't even try to press escape key.
        </ModalBody>
        <ModalFooter>
          <Button color="secondary" onClick={() => setVisible(false)}>
            Close
          </Button>
          <Button color="primary">Save changes</Button>
        </ModalFooter>
      </Modal>
    </>
  )
}

Scrolling long content

A modal too long for the viewport or device scrolls independently of the page itself.

JSX
import { useState } from 'react'
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle } from '@chassis-ui/react'

export const Example = () => {
  const [visible, setVisible] = useState(false)
  return (
    <>
      <Button onClick={() => setVisible(!visible)}>Launch demo modal</Button>
      <Modal visible={visible} onClose={() => setVisible(false)}>
        <ModalHeader>
          <ModalTitle>Modal title</ModalTitle>
        </ModalHeader>
        <ModalBody>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
        </ModalBody>
        <ModalFooter>
          <Button color="secondary" onClick={() => setVisible(false)}>
            Close
          </Button>
          <Button color="primary">Save changes</Button>
        </ModalFooter>
      </Modal>
    </>
  )
}

Set scrollable to scroll the modal body instead, keeping the header and footer fixed in place.

JSX
import { useState } from 'react'
import { Button, Modal, ModalBody, ModalFooter, ModalHeader, ModalTitle } from '@chassis-ui/react'

export const Example = () => {
  const [visible, setVisible] = useState(false)
  return (
    <>
      <Button onClick={() => setVisible(!visible)}>Launch demo modal</Button>
      <Modal scrollable visible={visible} onClose={() => setVisible(false)}>
        <ModalHeader>
          <ModalTitle>Modal title</ModalTitle>
        </ModalHeader>
        <ModalBody>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
          <p>
            Cras mattis consectetur purus sit amet fermentum. Cras justo odio, dapibus ac facilisis
            in, egestas eget quam. Morbi leo risus, porta ac consectetur ac, vestibulum at eros.
          </p>
          <p>
            Praesent commodo cursus magna, vel scelerisque nisl consectetur et. Vivamus sagittis
            lacus vel augue laoreet rutrum faucibus dolor auctor.
          </p>
          <p>
            Aenean lacinia bibendum nulla sed consectetur. Praesent commodo cursus magna, vel
            scelerisque nisl consectetur et. Donec sed odio dui. Donec ullamcorper nulla non metus
            auctor fringilla.
          </p>
        </ModalBody>
        <ModalFooter>
          <Button color="secondary" onClick={() => setVisible(false)}>
            Close
          </Button>
          <Button color="primary">Save changes</Button>
        </ModalFooter>
      </Modal>
    </>
  )
}

Tooltips and popovers

<Tooltip> and <Popover> can be placed within modals as needed. Closing a modal automatically dismisses any tooltips and popovers it contains.

JSX
import { useState } from 'react'
import {
  Button,
  Link,
  Modal,
  ModalBody,
  ModalFooter,
  ModalHeader,
  ModalTitle,
  Popover,
  Tooltip
} from '@chassis-ui/react'

export const Example = () => {
  const [visible, setVisible] = useState(false)
  return (
    <>
      <Button onClick={() => setVisible(!visible)}>Launch demo modal</Button>
      <Modal visible={visible} onClose={() => setVisible(false)}>
        <ModalHeader>
          <ModalTitle>Modal title</ModalTitle>
        </ModalHeader>
        <ModalBody>
          <h5>Popover in a modal</h5>
          <p>
            This
            <Popover title="Popover title" content="Popover body content is set in this property.">
              <Button>button</Button>
            </Popover>{' '}
            triggers a popover on click.
          </p>
          <hr />
          <h5>Tooltips in a modal</h5>
          <p>
            <Tooltip content="Tooltip">
              <Link>This link</Link>
            </Tooltip>{' '}
            and
            <Tooltip content="Tooltip">
              <Link>that link</Link>
            </Tooltip>{' '}
            have tooltips on hover.
          </p>
        </ModalBody>
        <ModalFooter>
          <Button color="secondary" onClick={() => setVisible(false)}>
            Close
          </Button>
          <Button color="primary">Save changes</Button>
        </ModalFooter>
      </Modal>
    </>
  )
}

Optional sizes

Modals have three optional sizes, available via modifier classes to be placed on a <Modal>. These sizes kick in at certain breakpoints to avoid horizontal scrollbars on narrower viewports.

SizeProperty sizeModal max-width
Small'sm'360px
DefaultNone560px
Large'lg'800px
Extra lg'xl'1140px
JSX
import { useState } from 'react'
import { Button, Modal, ModalBody, ModalHeader, ModalTitle } from '@chassis-ui/react'

export const Example = () => {
  const [visibleXL, setVisibleXL] = useState(false)
  const [visibleLg, setVisibleLg] = useState(false)
  const [visibleSm, setVisibleSm] = useState(false)
  return (
    <>
      <Button onClick={() => setVisibleXL(!visibleXL)}>Extra lg modal</Button>
      <Button onClick={() => setVisibleLg(!visibleLg)}>Large modal</Button>
      <Button onClick={() => setVisibleSm(!visibleSm)}>Small modal</Button>
      <Modal size="xl" visible={visibleXL} onClose={() => setVisibleXL(false)}>
        <ModalHeader>
          <ModalTitle>Extra lg modal</ModalTitle>
        </ModalHeader>
        <ModalBody>...</ModalBody>
      </Modal>
      <Modal size="lg" visible={visibleLg} onClose={() => setVisibleLg(false)}>
        <ModalHeader>
          <ModalTitle>Large modal</ModalTitle>
        </ModalHeader>
        <ModalBody>...</ModalBody>
      </Modal>
      <Modal size="sm" visible={visibleSm} onClose={() => setVisibleSm(false)}>
        <ModalHeader>
          <ModalTitle>Small modal</ModalTitle>
        </ModalHeader>
        <ModalBody>...</ModalBody>
      </Modal>
    </>
  )
}

Fullscreen modal

Set fullscreen to render a modal that covers the entire viewport, either always or below a given breakpoint.

Property fullscreenAvailability
trueAlways
'sm'

Below 576px

'md'

Below 768px

'lg'

Below 1024px

'xl'

Below 1280px

'2xl'

Below 1536px

JSX
import { useState } from 'react'
import { Button, Modal, ModalBody, ModalHeader, ModalTitle } from '@chassis-ui/react'

export const Example = () => {
  const [visible, setVisible] = useState(false)
  const [visibleSmall, setVisibleSmall] = useState(false)
  const [visibleMedium, setVisibleMedium] = useState(false)
  const [visibleLarge, setVisibleLarge] = useState(false)
  const [visibleXlg, setVisibleXlg] = useState(false)
  const [visible2xl, setVisible2xl] = useState(false)
  return (
    <>
      <Button onClick={() => setVisible(!visible)}>Full screen</Button>
      <Button onClick={() => setVisibleSmall(!visibleSmall)}>Full screen below sm</Button>
      <Button onClick={() => setVisibleMedium(!visibleMedium)}>Full screen below md</Button>
      <Button onClick={() => setVisibleLarge(!visibleLarge)}>Full screen below lg</Button>
      <Button onClick={() => setVisibleXlg(!visibleXlg)}>Full screen below xl</Button>
      <Button onClick={() => setVisible2xl(!visible2xl)}>Full screen below 2xl</Button>
      <Modal fullscreen visible={visible} onClose={() => setVisible(false)}>
        <ModalHeader>
          <ModalTitle>Full screen</ModalTitle>
        </ModalHeader>
        <ModalBody>...</ModalBody>
      </Modal>
      <Modal fullscreen="sm" visible={visibleSmall} onClose={() => setVisibleSmall(false)}>
        <ModalHeader>
          <ModalTitle>Full screen below sm</ModalTitle>
        </ModalHeader>
        <ModalBody>...</ModalBody>
      </Modal>
      <Modal fullscreen="md" visible={visibleMedium} onClose={() => setVisibleMedium(false)}>
        <ModalHeader>
          <ModalTitle>Full screen below md</ModalTitle>
        </ModalHeader>
        <ModalBody>...</ModalBody>
      </Modal>
      <Modal fullscreen="lg" visible={visibleLarge} onClose={() => setVisibleLarge(false)}>
        <ModalHeader>
          <ModalTitle>Full screen below lg</ModalTitle>
        </ModalHeader>
        <ModalBody>...</ModalBody>
      </Modal>
      <Modal fullscreen="xl" visible={visibleXlg} onClose={() => setVisibleXlg(false)}>
        <ModalHeader>
          <ModalTitle>Full screen below xl</ModalTitle>
        </ModalHeader>
        <ModalBody>...</ModalBody>
      </Modal>
      <Modal fullscreen="2xl" visible={visible2xl} onClose={() => setVisible2xl(false)}>
        <ModalHeader>
          <ModalTitle>Full screen below 2xl</ModalTitle>
        </ModalHeader>
        <ModalBody>...</ModalBody>
      </Modal>
    </>
  )
}

Accessibility

The <dialog> element carries an implicit role="dialog" when opened with showModal(). <Modal> is labelled by its <ModalTitle> automatically — render one anywhere inside and aria-labelledby names it, under a generated id or the id you give the title, giving the modal an accessible name with no manual wiring. A modal with no title has no aria-labelledby: name it with aria-label, which also wins over a title, or set your own aria-labelledby on <Modal> to point elsewhere.

Focus moves to the modal on open — to the first element with data-autofocus (or an autofocus attribute in server-rendered HTML), otherwise to the modal itself. React's autoFocus prop isn't enough in a page rendered in the browser: React writes no autofocus attribute there. After the modal closes, focus returns to whichever element was focused when it opened.

API

PropTypeDefault
backdrop?boolean | "static"true
Show a backdrop while the modal is open. 'static' blocks closing on backdrop click (the modal bounces instead). false is intentionally not equivalent to modal={false} — unlike Drawer, whose vanilla chassis-css counterpart derives modality from backdrop, chassis-css's vanilla Dialog only ever reads backdrop to distinguish 'static' from everything else; modal alone decides showModal()/show(). This mirrors that faithfully rather than reintroducing an inconsistency with the vendored behavior.
className?string—
A string of all className you want applied to the base component.
defaultVisible?boolean—
Whether the modal is open when it first renders. Such a modal is uncontrolled: it closes itself on a close request. Use visible to open and close it from outside.
fullscreen?boolean | "sm" | "md" | "lg" | "xl" | "2xl"—
Set modal to cover the entire user viewport. A breakpoint value goes fullscreen only below that breakpoint.
instant?boolean—
Disable the open/close transition entirely.
keyboard?booleantrue
Closes the modal when the escape key is pressed.
modal?booleantrue
Open with showModal() (top-layer, native backdrop). Set false to open with show() instead — no backdrop, and the page behind the modal stays interactive.
onClose?(() => void)—
Callback fired when the modal requests to be closed (escape, backdrop click, or close button).
onClosePrevented?(() => void)—
Callback fired when a close attempt is blocked (static backdrop click, or escape with keyboard=false).
onHidden?(() => void)—
Callback fired after the exit transition completes and the modal is fully hidden.
onShow?(() => void)—
Callback fired when the modal starts to open.
onShown?(() => void)—
Callback fired after the entry transition completes and the modal is fully visible.
onVisibleChange?((visible: boolean) => void)—
Callback fired with false when the modal requests to be closed, at the same moments as onClose. It makes visible and onVisibleChange a pair that takes a state setter.
open?boolean—
Renders the modal open from its first paint, server HTML included, as a non-modal dialog: a static preview. It stays open until a close request. Use visible or defaultVisible to open it as a modal dialog.
scrollable?boolean—
Create a scrollable modal — the header and footer stay fixed while the body scrolls.
size?"sm" | "lg" | "xl"—
Size the component sm, lg, or extra lg.
visible?boolean—
Whether the modal is open. The modal is then controlled: a close request only fires onClose and onVisibleChange, and the modal closes when this becomes false.

ModalBody

PropTypeDefault
asChild?boolean—
Render the single child element in place of this component's own element, with this component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log in</Link></Button>. The alternative to component for React Server Components — an element can be passed from the server to this package's client components, a component reference (component={Link}) can't.
className?string—
A string of all className you want applied to the base component.
component?ElementType—
Component used for the root node. Either a string to use a HTML element or a component.
ref?any—

ModalFooter

PropTypeDefault
asChild?boolean—
Render the single child element in place of this component's own element, with this component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log in</Link></Button>. The alternative to component for React Server Components — an element can be passed from the server to this package's client components, a component reference (component={Link}) can't.
className?string—
A string of all className you want applied to the base component.
component?ElementType—
Component used for the root node. Either a string to use a HTML element or a component.
ref?any—
stacked?boolean—
Stack the footer actions as full-width columns instead of a right-aligned row.

ModalHeader

PropTypeDefault
asChild?boolean—
Render the single child element in place of this component's own element, with this component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log in</Link></Button>. The alternative to component for React Server Components — an element can be passed from the server to this package's client components, a component reference (component={Link}) can't.
className?string—
A string of all className you want applied to the base component.
closeButton?boolean—
Add a close button component to the header.
closeLabel?string—
Overrides the close button's accessible name (defaults to 'Close'). Set this for non-English UIs.
component?ElementType—
Component used for the root node. Either a string to use a HTML element or a component.
ref?any—

ModalTitle

PropTypeDefault
asChild?boolean—
Render the single child element in place of this component's own element, with this component's classes, props and ref merged onto it: <Button asChild><Link href="/login">Log in</Link></Button>. The alternative to component for React Server Components — an element can be passed from the server to this package's client components, a component reference (component={Link}) can't.
className?string—
A string of all className you want applied to the base component.
component?ElementType—
Component used for the root node. Either a string to use a HTML element or a component.
ref?any—