Modal
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.
Modal components
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.
<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.
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.
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.
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.
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.
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.
| Size | Property size | Modal max-width |
|---|---|---|
| Small | 'sm' | 360px |
| Default | None | 560px |
| Large | 'lg' | 800px |
| Extra lg | 'xl' | 1140px |
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 fullscreen | Availability |
|---|---|
true | Always |
'sm' | Below |
'md' | Below |
'lg' | Below |
'xl' | Below |
'2xl' | Below |
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
Modal
| Prop | Type | Default |
|---|---|---|
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? | boolean | true |
| Closes the modal when the escape key is pressed. | ||
modal? | boolean | true |
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
| Prop | Type | Default |
|---|---|---|
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
| Prop | Type | Default |
|---|---|---|
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
| Prop | Type | Default |
|---|---|---|
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
| Prop | Type | Default |
|---|---|---|
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 | — |