Modal
A modal is an overlay dialog that focuses the user’s attention on a specific task or content.
Ready to use
A Modal is a focused container that appears above the page to display essential, interruptive, or task-related content without navigating away from the current view. It temporarily disables interaction with the underlying interface and requires the user to complete an action before returning to the previous context.
Anatomy
Section titled “Anatomy”- Modal
- Frame
- Close button
- Content
When to use it
Section titled “When to use it”Use the Modal when:
- The user must take an action before continuing (confirm, acknowledge, complete a task).
- You need to display content that requires full focus without distractions.
- The interaction must not be missed (legal notices, blocking states).
Avoid the Modal when:
- The information is optional or lightweight, and could sit inline or in a Popover.
- The interruption breaks the user flow unnecessarily.
- A non-blocking alternative (sheet, inline block, banner) would feel smoother.
Properties
Section titled “Properties”Variant
Three composition shapes: Full screen (large surfaces such as comparison views), With image (a media-led modal where the visual anchors the message), and Without image (text-and-actions, the default for confirmations and forms).
Close button
Mandatory in every variant — a tertiary Button-Main at the LG size with the close icon. Optional dismissals like tapping outside the modal are not defined at component level and must not replace the visible close button.
Image
Optional media slot at the top of the modal, sourced from the Image component for consistent responsive behaviour. Recommended aspect ratio is 16:9 or 9:16.
Content slot
Mandatory and flexible — holds text, images, simple forms, or component instances. The modal supplies the container and behaviour; the slot’s content follows its own component guidelines.
Action area
Holds Primary, Secondary, Tertiary, and Link buttons drawn from the Button Main and Link Button components. Layout reflows from horizontal (desktop and tablet) to vertical (mobile, or whenever long labels need the room).
Backdrop
Mandatory — the modal sits above a semi-transparent Backdrop that blocks interaction with underlying content and isolates focus. Tokens for colour, opacity, and elevation live in the Backdrop component.
Platform considerations
Section titled “Platform considerations”Desktop
The modal appears as a centred dialog with a backdrop. Horizontal action layout is the default, with buttons side-by-side following the standard hierarchy.
Tablet
Same centred dialog with horizontal actions. When labels are long enough to wrap, switch to vertical actions to keep the buttons readable.
Mobile
The modal automatically adopts a bottom-sheet layout, sliding up from the bottom. Actions stack vertically so the touch targets stay in the thumb zone. Structure, slot, close button, and action sets stay identical — only the layout changes.
Best practices
Section titled “Best practices”Treat the modal as a focus tool — the moment it appears, nothing else should pull the user’s attention.
Do
Use a modal when the user must focus on a single task or piece of information, keep the close button visible and accessible, trap focus until the modal closes and return focus to the trigger on dismiss, and keep all content inside the modal reachable in a logical reading order.
Don't
Don’t use a modal for long or complex content (consider a dedicated page when copy grows), don’t stack multiple modals, and don’t rely on the backdrop alone for dismissal — the close button is mandatory.
Content guidelines
Section titled “Content guidelines”The modal’s first line of copy carries the most weight — keep it short and name the decision the user needs to make. Body copy explains the consequence (“Deleting will remove all saved settings”); action buttons name the verb (“Delete”, “Cancel”). Avoid filler (“Are you sure you want to…?”) in favour of direct phrasing, and keep button labels consistent across modals so the choices recognise quickly.
Styles
Section titled “Styles”A modal is an overlay dialog that focuses the user’s attention on a specific task or content.
<div class="tng-modal"> <div class="tng-modal-panel"> <button class="tng-button is-tertiary is-ghost"> <span>Close</span> <i class="tng-icon icon-close" aria-hidden="true"></i> </button> <div class="tng-modal-content"> <div class="tng-slot is-primary"></div> </div> </div></div>Fullscreen
Section titled “Fullscreen”You can use the positioning utilities to create a fullscreen modal.
<div class="tng-modal | p-absolute at-maximum"> <div class="tng-modal-panel"> <button class="tng-button is-tertiary is-ghost"> <span>Close</span> <i class="tng-icon icon-close" aria-hidden="true"></i> </button> <div class="tng-modal-content"> <div class="tng-slot is-primary"></div> </div> </div></div>With Scroll
Section titled “With Scroll”When a modal contains potentially more content than fits in the viewport, you can use the overflow utility to make the content area scrollable.
<div class="tng-modal" style="max-block-size: 300px"> <div class="tng-modal-panel"> <button class="tng-button is-tertiary is-ghost"> Close <i class="tng-icon icon-close" aria-hidden="true"></i> </button> <div class="tng-overflow-scroll is-block"> <div class="tng-modal-content"> <p class="tng-text-body">…</p> <p class="tng-text-body">…</p> </div> </div> </div></div>With Image
Section titled “With Image”<div class="tng-modal"> <div class="tng-frame"> <div class="tng-slot"></div> </div> <div class="tng-modal-panel"> <button class="tng-button is-tertiary is-ghost"> Close <i class="tng-icon icon-close" aria-hidden="true"></i> </button> <div class="tng-modal-content"> <div class="tng-slot is-primary"></div> </div> </div></div>Recipes
Section titled “Recipes”Modal Dialog
Section titled “Modal Dialog”Import the Modal component from @tmedxp/react-components.
Properties
Section titled “Properties”ModalProperties extends <PropsWithChildren<Omit<ComponentProps<'dialog'>, 'open'>>>.
| Prop | Type | Description | Optional |
|---|---|---|---|
instance |
ModalInstance |
The modal instance used to open / close | ❌ |
position |
ModalPosition |
Position: 'ABSOLUTE' | 'FIXED' | 'RELATIVE' |
✅ |
placement |
ModalPlacement |
Placement: centered, top, bottom, etc. | ✅ |
labels |
ModalLabels |
Collection of labels used by the modal | ❌ |
frameContent |
React.ReactElement |
Content of tng-frame container if framed | ✅ |
Example without frame
Section titled “Example without frame”import { Modal, ModalPlacement, ModalPosition, useModalInstance, Button,} from '@tmedxp/react-components';
const modalInstance = useModalInstance();const ModalExample = () => { return ( <> <Button buttonType="button" buttonSize="md" buttonStyle="primary" onClick={modalInstance.open} text="Open modal overlay" /> <Modal instance={modalInstance} position={ModalPosition.ABSOLUTE} placement={ModalPlacement.CENTER} labels={{ Close: 'Close overlay' }} > <p class="tng-text-body"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. </p> </Modal> </> );};
export { ModalExample };Example with frame
Section titled “Example with frame”import { Modal, ModalPlacement, ModalPosition, useModalInstance, Button,} from '@tmedxp/react-components';
const modalInstance = useModalInstance();const frameContent = <div className="tng-slot"></div>;const ModalExample = () => { return ( <> <Button buttonType="button" buttonSize="md" buttonStyle="primary" onClick={modalInstance.open} text="Open framed modal overlay" /> <Modal instance={modalInstance} position={ModalPosition.ABSOLUTE} placement={ModalPlacement.CENTER} labels={{ Close: 'Close overlay' }} frameContent={frameContent} > <p class="tng-text-body"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. </p> </Modal> </> );};
export { ModalExample };useDialog hook
Section titled “useDialog hook”Pairs with Modal to manage open/close state.
Properties
Section titled “Properties”id— a string referencing the dialog.isModal—trueto show as a modal.onSubmit— invoked when the dialog is closing.
Returned API
Section titled “Returned API”reference— the dialog’s DOM reference.properties— the dialog’s properties.softClose()— close the dialog.openOverlay()— open the dialog.
Example
Section titled “Example”const dialog = useDialog({ id: 'overlay', isModal: true, onSubmit: () => alert('The dialog has been closed'),});
dialog.openOverlay();
dialog.softClose();The Modal follows the WAI-ARIA APG Dialog (Modal) pattern — an interruptive window that takes over the page until the user acts on it — and meets WCAG 2.1 AA. An accessible modal announces itself to assistive technology as a dialog, traps focus inside itself while it is open, returns focus to the trigger when it closes, and gives its title, close button, and actions each an accessible name — so the task it holds is clear whether the user is looking at it, listening to it, or tabbing through it.
For designers
Section titled “For designers”- The modal surface, title, close button, and action buttons must maintain accessible contrast on every supported surface, including over the backdrop. Source: WCAG 1.4.3 Contrast (Minimum).
- Meaning must never be conveyed by colour alone — pair it with text, an icon, or a shape. Source: WCAG 1.4.1 Use of Color.
- Focus styles must stay visible on the close button and every action, meet contrast requirements on the modal surface, and be clearly distinguishable from hover. Source: WCAG 2.4.7 Focus Visible.
- Use semantic colour tokens (e.g.
foreground/neutral/default) rather than fixed values, so the component adapts correctly across themes and surfaces.
For developers
Section titled “For developers”Always render the modal on a native <dialog> element and open it with showModal(). That single decision earns the whole APG Dialog contract from the browser: the dialog is promoted to the top layer (no z-index conflicts), a ::backdrop renders automatically, focus is trapped inside while it is open, focus returns to the previously focused element on close, and Escape closes it — with aria-modal="true" implied so the rest of the page drops out of the accessibility tree. To stop pointer events leaking to the page underneath, also set document.body.inert = true while the dialog is open and reset it on close, as shown in the Modal Dialog recipe. Wrap the content in a <form method="dialog"> so any close or confirm <button> dismisses the dialog and exposes its value via dialog.returnValue without custom script. The dialog’s, trigger’s, and close button’s roles, names, and states are in the Labelling elements section below.
Keyboard interaction
Section titled “Keyboard interaction”| Key | Action |
|---|---|
| Tab / Shift + Tab | Cycle focus through the focusable elements inside the dialog |
| Escape | Close the dialog and return focus to the trigger |
Interactive targets — the close button and every action — must be at least 44 px high for comfortable touch use, comfortably clearing the WCAG AA minimum. Source: WCAG 2.5.8 Target Size (Minimum).
Labelling elements
Section titled “Labelling elements”Give every element the role, name, and state assistive technology needs. The demo stacks the trigger and the open dialog so the two can be read together — the content slot is a placeholder, since consumer-slotted content carries its own component’s semantics.
- Trigger
- Dialog
- Close button
Trigger
The button that opens the modal. Set aria-controls to the dialog’s id so the relationship is exposed, and give it a clear accessible name from its visible label — name the task it opens, not just “Open”.
Dialog
The native <dialog> carries the dialog role, and showModal() implies aria-modal="true". Name it with aria-labelledby pointing to the visible title (or aria-label when there is no visible title), so it never announces as an unnamed dialog. Add aria-describedby to reference a description paragraph when extra context helps.
Close button
Mandatory in every variant. Its accessible name must read as “Close” — from visible text, or aria-label="Close" when it is icon-only, with the close icon marked aria-hidden="true" so it is not announced twice.
Source: WCAG 4.1.2 Name, Role, Value.