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

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

PropTypeDefaultDescription
openbooleanWhether the dialog is shown.
onClose() => voidRequested close (scrim click / Escape).
titlestringAccessible title (heading + aria-labelledby).
closeOnScrimClickbooleantrueClose when the scrim is clicked.
childrenReactNodeDialog body.

Web Components@ghds/web-components

PropTypeDefaultDescription
openbooleanWhether the dialog is shown (reflected).
headingstringAccessible title.
closeOnScrimClickbooleantrueClose on backdrop click.
close eventCustomEventDispatched on Escape / backdrop click.

React Native@ghds/react-native

PropTypeDefaultDescription
openbooleanWhether the dialog is shown.
onClose() => voidRequested close (scrim tap / back).
titlestringAccessible title.
closeOnScrimClickbooleantrueClose when the scrim is tapped.
testIDstringTest 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

Open dialog

This action cannot be undone.