Modal
A hand-drawn modal dialog with a scrim, focus trap, and Escape-to-close.
Overview
A Modal (dialog) interrupts the flow to focus attention on a single task — a confirmation, a short
form, a detail view. It traps focus and dims the rest of the page with a scrim until dismissed.
Anatomy
A full-viewport scrim (comp.modal.scrim.*) behind a centered sketchy panel (@ghds/sketch-core)
with an optional title and body. It is role="dialog" + aria-modal="true", wired to the title via
aria-labelledby.
Variants & States
| State | Status | Notes |
|---|---|---|
| Open | Implemented | Portalled above the page; focus moves inside and is trapped. |
| Closed | Implemented | Renders nothing (React) / not shown (WC/RN). |
| Scrim dismiss | Implemented | Clicking the scrim requests close (opt out with closeOnScrimClick={false}). |
Usage
Do
- Keep it controlled: own the open state and close from onClose.
- Give it a title so it has an accessible name.
- Reserve modals for focused, interrupting tasks; keep their content short.
Don't
- Don't stack multiple modals.
- Don't put long, scrollable page content in a modal — use a page instead.
Accessibility
- React: portals to
document.body;role="dialog"+aria-modal; focus moves to the first focusable element on open, Tab is trapped within the dialog, focus is restored to the previously focused element on close, Escape closes, and body scroll is locked. - Web Components: built on the native
<dialog>(showModal()), which provides focus trapping, the top layer, and Escape for free; the::backdropis the scrim. Dispatches acloseevent. - React Native: built on RN’s
Modal(focus containment + hardware back);role="dialog"+accessibilityViewIsModal. - See the Accessibility Guide.
| Key | Action |
|---|---|
Tab / Shift+Tab | Cycle focus within the dialog (trapped). |
Escape | Request close. |
Content
A concise title and focused body. Put primary/secondary actions at the end of the body.
Props API
React@ghds/react
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Whether the dialog is shown. |
onClose | () => void | — | Requested close (scrim click / Escape). |
title | string | — | Accessible title (heading + aria-labelledby). |
closeOnScrimClick | boolean | true | Close when the scrim is clicked. |
children | ReactNode | — | Dialog body. |
Web Components@ghds/web-components
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Whether the dialog is shown (reflected). |
heading | string | — | Accessible title. |
closeOnScrimClick | boolean | true | Close on backdrop click. |
close event | CustomEvent | — | Dispatched on Escape / backdrop click. |
React Native@ghds/react-native
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | — | Whether the dialog is shown. |
onClose | () => void | — | Requested close (scrim tap / back). |
title | string | — | Accessible title. |
closeOnScrimClick | boolean | true | Close when the scrim is tapped. |
testID | string | — | Test handle for queries. |
Code examples
// React — @ghds/react
import { Modal } from '@ghds/react/modal';
import { Button } from '@ghds/react/button';
import { useState } from 'react';
const [open, setOpen] = useState(false);
<>
<Button onClick={() => setOpen(true)}>Open dialog</Button>
<Modal open={open} onClose={() => setOpen(false)} title="Delete item?">
<p>This action cannot be undone.</p>
<Button variant="danger" onClick={() => setOpen(false)}>Delete</Button>
</Modal>
</>;
<!-- Web Components — @ghds/web-components -->
<script type="module">
import '@ghds/web-components/modal';
const modal = document.getElementById('dialog');
// `heading`, not `title`; `open` is a property; listen for the `close` event
modal.open = true;
modal.addEventListener('close', () => {
modal.open = false;
});
</script>
<gh-modal id="dialog" heading="Delete item?">
<p>This action cannot be undone.</p>
</gh-modal>
// React Native — @ghds/react-native
import { Modal } from '@ghds/react-native/modal';
import { Text } from '@ghds/react-native/theme';
import { useState } from 'react';
const [open, setOpen] = useState(false);
<Modal open={open} onClose={() => setOpen(false)} title="Delete item?">
<Text>This action cannot be undone.</Text>
</Modal>;
Live Demo
React @ghds/react
Web Components @ghds/web-components
This action cannot be undone.