Modal
feedbackUSWDS-derivedinteractive
Dialog overlay with focus trapping, backdrop, and keyboard management.
Reference: USWDS documentation ↗
Variants
Default Modal
Are you sure?
This is the default modal content.
<div>
<button type="button" class="flex-button" aria-controls="modal-default" data-open-modal="true">Open default modal</button>
<flex-modal id="modal-default" hidden="">
<div class="flex-modal__overlay">
</div>
<div class="flex-modal__content" role="dialog" aria-modal="true" aria-labelledby="modal-default-heading" aria-describedby="modal-default-body">
<div class="flex-modal__main">
<h2 class="flex-modal__heading" id="modal-default-heading">Are you sure?</h2>
<div class="flex-modal__body" id="modal-default-body">
<p>This is the default modal content.</p>
</div>
<div class="flex-modal__footer">
<button type="button" class="flex-button" data-close-modal="true">Close</button>
</div>
</div>
<button type="button" class="flex-modal__close" aria-label="Close this modal" data-close-modal="true">
<svg class="flex-icon" aria-hidden="true" focusable="false">
<use href="/static/sprite.svg#close">
</use>
</svg>
</button>
</div>
</flex-modal>
</div>Forced Action Modal
Action required
You must take an action. This modal cannot be closed by clicking the overlay or pressing Escape.
<div>
<button type="button" class="flex-button" aria-controls="modal-forced" data-open-modal="true">Open forced action modal</button>
<flex-modal id="modal-forced" hidden="" data-forced-action="">
<div class="flex-modal__overlay">
</div>
<div class="flex-modal__content" role="dialog" aria-modal="true" aria-labelledby="modal-forced-heading" aria-describedby="modal-forced-body">
<div class="flex-modal__main">
<h2 class="flex-modal__heading" id="modal-forced-heading">Action required</h2>
<div class="flex-modal__body" id="modal-forced-body">
<p>You must take an action. This modal cannot be closed by clicking the overlay or pressing Escape.</p>
</div>
<div class="flex-modal__footer">
<button type="button" class="flex-button" data-close-modal="true">Accept and close</button>
</div>
</div>
</div>
</flex-modal>
</div>Large Modal
Large modal
This is a large modal with wider max-width.
<div>
<button type="button" class="flex-button" aria-controls="modal-large" data-open-modal="true">Open large modal</button>
<flex-modal id="modal-large" hidden="" data-size="large">
<div class="flex-modal__overlay">
</div>
<div class="flex-modal__content" role="dialog" aria-modal="true" aria-labelledby="modal-large-heading" aria-describedby="modal-large-body">
<div class="flex-modal__main">
<h2 class="flex-modal__heading" id="modal-large-heading">Large modal</h2>
<div class="flex-modal__body" id="modal-large-body">
<p>This is a large modal with wider max-width.</p>
</div>
<div class="flex-modal__footer">
<button type="button" class="flex-button" data-close-modal="true">Close</button>
</div>
</div>
<button type="button" class="flex-modal__close" aria-label="Close this modal" data-close-modal="true">
<svg class="flex-icon" aria-hidden="true" focusable="false">
<use href="/static/sprite.svg#close">
</use>
</svg>
</button>
</div>
</flex-modal>
</div>Contract
Class mapping
| USWDS | Flex | Notes |
|---|---|---|
usa-modal-wrapper / usa-modal-overlay | <flex-modal> (custom element) + .flex-modal__overlay | Modal wrapper and overlay backdrop |
usa-modal | .flex-modal__content[role="dialog"] | Dialog content container |
usa-modal__content | .flex-modal__content | Content wrapper with column-reverse layout |
usa-modal__main | .flex-modal__main | Main content area with heading, body, footer |
usa-modal__heading | .flex-modal__heading | Modal heading |
usa-modal__footer | .flex-modal__footer | Modal footer with action buttons |
usa-modal__close | .flex-modal__close[data-close-modal] | Close button with X icon |
usa-modal--lg | flex-modal[data-size="large"] | Large modal variant with wider max-width |
data-force-action | data-forced-action | Forced action prevents overlay click, escape, hides close button |
Verified properties
displaypositionmax-widthborder-radiusbackground-colorIntentional differences
visibility: ours = hidden (via hidden attr), USWDS = controlled via is-hidden/is-visible classes
We use the native hidden attribute; USWDS uses JS class toggling with visibility transitions
Behavior promises
- ✓ Trigger button opens modal and sets aria-expanded
- ✓ Close button closes modal and restores focus to trigger
- ✓ Escape key closes modal (unless forced action)
- ✓ Overlay click closes modal (unless forced action)
- ✓ Focus is trapped within modal using Tab/Shift+Tab
- ✓ First focusable element receives focus on open
- ✓ Scroll lock applied to body when modal is open
- ✓ Forced action prevents escape, overlay click, and hides close button
Source CSS
/* flex-modal — USWDS Modal conformance
Dialog overlay with focus trapping, backdrop, and keyboard management */
flex-modal {
display: none;
&:not([hidden]) {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
inset: 0;
z-index: 99999;
}
&[data-size="large"] {
& .flex-modal__content {
/* stylelint-disable-next-line declaration-property-value-allowed-list -- Modal size variants are component-intrinsic sizes, not content-width. */
max-inline-size: 55rem;
}
& .flex-modal__main {
padding-block: 1.25rem 4rem;
inline-size: 100%;
/* stylelint-disable-next-line declaration-property-value-allowed-list -- Modal size variants are component-intrinsic sizes, not content-width. */
max-inline-size: 40rem;
}
}
}
.flex-modal__overlay {
position: fixed;
inset: 0;
background: rgb(0 0 0 / 70%);
}
.flex-modal__content {
position: relative;
display: inline-flex;
flex-direction: column-reverse;
background: var(--flex-color-surface);
border-radius: 0.5rem;
/* stylelint-disable-next-line declaration-property-value-allowed-list -- Modal size variants are component-intrinsic sizes, not content-width. */
max-inline-size: 30rem;
inline-size: 100%;
max-block-size: calc(100vh - 3rem);
overflow-y: auto;
z-index: 1;
padding-block-start: 2rem;
margin: 1.25rem auto;
}
.flex-modal__main {
margin: 0 auto;
padding: 0.5rem 2rem 2rem;
}
.flex-modal__heading {
font-family: var(--flex-font-serif, Georgia, 'Times New Roman', serif);
font-size: 1.34rem;
line-height: 1.4;
margin-block-start: 0;
}
.flex-modal__body {
font-size: 1.06rem;
line-height: 1.5;
color: var(--flex-color-text);
}
.flex-modal__footer {
margin-block-start: 1.5rem;
}
.flex-modal__close {
display: flex;
align-items: center;
align-self: flex-end;
flex-shrink: 0;
background-color: transparent;
border: 0;
color: var(--flex-color-text-muted);
font-size: 0.93rem;
margin: -2rem 0 0 auto;
padding: 0.25rem;
inline-size: auto;
cursor: pointer;
&:hover,
&:active {
background-color: transparent;
color: var(--flex-color-text);
}
&:focus {
outline: 0.25rem solid var(--flex-color-focus);
outline-offset: 0;
}
& .flex-icon {
block-size: 2rem;
inline-size: 2rem;
margin: 2px 2px 0 0;
}
}
A digital services project by Flexion