Skip to main content

Modal

Focus attention on one task or message with a modal. Compare default, large, and required-action dialogs.

Examples

Default

Breakpoints

Mobile

320px

Mobile Lg

480px

Tablet

640px

Desktop

1024px
modal-default-demo.tsx
import { ButtonGroup, Modal } from '@uswds-tailwind/react'
import { useState } from 'react'
export default function ModalDefaultDemo() {
const [open, setOpen] = useState(false)
return (
<Modal.Root open={open} onOpenChange={event => setOpen(event.open)}>
<Modal.Trigger>Open default modal</Modal.Trigger>
<Modal.Backdrop />
<Modal.Positioner>
<Modal.Content>
<Modal.Body>
<Modal.Title>
<h2>Are you sure you want to continue?</h2>
</Modal.Title>
<Modal.Description>
<p>You have unsaved changes that will be lost.</p>
</Modal.Description>
<Modal.Footer>
<ButtonGroup.Root>
<ButtonGroup.Button onClick={() => setOpen(false)}>Continue without saving</ButtonGroup.Button>
<ButtonGroup.Button unstyled className="p-3" onClick={() => setOpen(false)}>Go back</ButtonGroup.Button>
</ButtonGroup.Root>
</Modal.Footer>
</Modal.Body>
<Modal.CloseTrigger aria-label="Close this window" />
</Modal.Content>
</Modal.Positioner>
</Modal.Root>
)
}

Large

Breakpoints

Mobile

320px

Mobile Lg

480px

Tablet

640px

Desktop

1024px
modal-large-demo.tsx
import { ButtonGroup, Modal } from '@uswds-tailwind/react'
import { useState } from 'react'
export default function ModalLargeDemo() {
const [open, setOpen] = useState(false)
return (
<Modal.Root size="lg" open={open} onOpenChange={event => setOpen(event.open)}>
<Modal.Trigger>Open large modal</Modal.Trigger>
<Modal.Backdrop />
<Modal.Positioner>
<Modal.Content>
<Modal.Body>
<Modal.Title>
<h2>Are you sure you want to continue?</h2>
</Modal.Title>
<Modal.Description>
<p>You have unsaved changes that will be lost.</p>
</Modal.Description>
<Modal.Footer>
<ButtonGroup.Root>
<ButtonGroup.Button onClick={() => setOpen(false)}>Continue without saving</ButtonGroup.Button>
<ButtonGroup.Button unstyled className="p-3" onClick={() => setOpen(false)}>Go back</ButtonGroup.Button>
</ButtonGroup.Root>
</Modal.Footer>
</Modal.Body>
<Modal.CloseTrigger aria-label="Close this window" />
</Modal.Content>
</Modal.Positioner>
</Modal.Root>
)
}

Forced Action

Breakpoints

Mobile

320px

Mobile Lg

480px

Tablet

640px

Desktop

1024px
modal-forced-action-demo.tsx
import { ButtonGroup, Modal } from '@uswds-tailwind/react'
import { useState } from 'react'
export default function ModalForcedActionDemo() {
const [open, setOpen] = useState(false)
return (
<Modal.Root forceAction open={open} onOpenChange={event => setOpen(event.open)}>
<Modal.Trigger>Open modal with forced action</Modal.Trigger>
<Modal.Backdrop />
<Modal.Positioner>
<Modal.Content>
<Modal.Body>
<Modal.Title>
<h2>Your session will end soon.</h2>
</Modal.Title>
<Modal.Description>
<p>
You've been inactive for too long. Please choose to stay signed in or sign out.
Otherwise, you'll be signed out automatically in 5 minutes.
</p>
</Modal.Description>
<Modal.Footer>
<ButtonGroup.Root>
<ButtonGroup.Button onClick={() => setOpen(false)}>Yes, stay signed in</ButtonGroup.Button>
<ButtonGroup.Button unstyled className="p-3" onClick={() => setOpen(false)}>Sign out</ButtonGroup.Button>
</ButtonGroup.Root>
</Modal.Footer>
</Modal.Body>
</Modal.Content>
</Modal.Positioner>
</Modal.Root>
)
}

Component API

Root provides context. Keep Backdrop and Positioner beside the trigger inside Root, and place Content inside Positioner. Give the dialog an accessible name through Title or aria-label on Root.

Root

Root controls whether the dialog is open and sets its size.

PropTypeDefaultDescription
size"default" | "lg"defaultChanges content width and body/title spacing.
openboolean—Controlled open state; update it after a change callback.
onOpenChange({ open: boolean }) => void—Called when an open-state change is requested.
forceActionbooleanfalsePrevents Escape and outside interaction from dismissing the dialog. Supply an explicit action.
aria-labelstring—Names the dialog directly and takes precedence over the Title relationship.
ids{ root?: string; trigger?: string; backdrop?: string; positioner?: string; content?: string; closeTrigger?: string; title?: string; description?: string }—Overrides generated part IDs. Use Root IDs when a label or external control needs a stable input or trigger target.

The dialog traps focus, prevents background scrolling, and restores focus after closing. These behaviors are managed automatically; the dialog role is fixed to dialog.

Trigger<button>

Opens the dialog and uses the default Button styling. Accepts native button props.

AttributeValuePurpose
data-scope, data-partmodal, triggerSelect the trigger.
data-stateopen | closedStyle the open and closed states.

Backdrop<div>

Covers the page behind the dialog. Clicking it closes the dialog when forceAction is false.

AttributeValuePurpose
data-scope, data-partmodal, backdropSelect the backdrop.
data-stateopen | closedStyle the open and closed states.

Positioner<div>

Centers the dialog content.

AttributeValuePurpose
data-stateopen | closedStyle the open and closed states.

Content<div>

Holds the dialog content. Use Title or aria-label on Root to name the dialog.

AttributeValuePurpose
data-scope, data-partmodal, contentSelect the dialog content.
data-stateopen | closedStyle the open and closed states.

Title<div>

Names the dialog. Put a heading inside it at the appropriate page level.

AttributeValuePurpose
data-scope, data-partmodal, titleSelect the title.

Description<div>

Provides an optional description for the dialog.

AttributeValuePurpose
data-scope, data-partmodal, descriptionSelect the description.

Body<div>

Spacing responds to Root's size variant.

Provides spacing for dialog actions.

CloseTrigger<button>

Closes the dialog. Supply aria-label when using the default icon-only button.

PropTypeDescription
aria-labelstringOverrides the default accessible name.
AttributeValuePurpose
data-scope, data-partmodal, close-triggerSelect the close button.

Accessibility

The dialog manages focus, labeling, and closed-state visibility. Provide a useful title or label, meaningful action names, and an explicit way to finish a forced-action dialog.