Components
Modal
StableModals display content that requires user attention or input without navigating away from the current page. They block all other interaction until dismissed.
Purpose
A modal is a focused interruption. Use it when the user must attend to something before they can continue — confirming a destructive action, completing a short form, or reviewing important information. Because it blocks all other interaction and adds a cognitive load, it should be reserved for cases where inline content genuinely cannot do the job.
Use a modal for:
- Holiday enquiry forms triggered from a CTA button
- Cancellation or deletion confirmations
- Terms acceptance required before proceeding
- Image lightboxes (show a destination image at full scale)
Do not use a modal for:
- Complex multi-step journeys (use a dedicated page or wizard instead)
- Informational content the user did not request (marketing modals, cookie banners)
- Navigation — modals do not participate in browser history
- Content that can be shown inline without interrupting the flow
Usage
Choose a Modal over a Popover or Drawer when the content genuinely requires the user's full attention and must block other interaction — a confirmation, a short form, or a lightbox. A Popover is lighter-weight and non-blocking; a Drawer better suits longer or navigation-adjacent content that still benefits from staying anchored to one edge of the screen.
Modals are triggered directly by a user action — a button click or link tap — never automatically on page load or after a timer. They typically appear from a CTA on a card or page (enquiry forms), from a destructive action's confirm step, or from an image thumbnail (lightbox).
Anatomy
Enquire about this holiday
One of our Personal Travel Experts will be in touch within one working day.
- 1Backdrop — Semi-transparent black overlay covering the full viewport (bg-black/50 + backdrop-blur-sm). Clicking the backdrop calls onClose.
- 2Panel — White rounded-lg container, centred on screen. Width set by the size prop. Shadow-xl elevates it above the backdrop.
- 3Header — Contains the title (h2) and the close button (X icon). Separated from the body by a border-bottom.
- 4Close button — X icon, top-right of header. Calls onClose. Can be hidden via hideCloseButton (rare — always provide another close affordance).
- 5Description — Optional subtitle below the title, above the body. Linked to the panel via aria-describedby.
- 6Body — Scrollable content area. Accepts any ReactNode as children.
- 7Footer — Optional slot for action buttons. Separated from the body by a border-top. Renders only when the footer prop is provided.
Variants
Informational
Title + body text + single close action. Used for showing terms, disclosures, or contextual help the user asked for.
ATOL Financial Protection
Many of the flights and flight-inclusive holidays on this website are financially protected by the ATOL scheme.
Confirmation
Title + body description + two actions (confirm + cancel). Used for destructive or irreversible operations. The primary action should be the danger variant Button.
Cancel this booking?
This action cannot be undone. Cancellation fees may apply.
Form modal
Title + form fields + submit + cancel. Used for the holiday enquiry form triggered from destination and offer pages. The submit button is full-width in the footer.
Enquire about this holiday
We will respond within one working day.
Sizes
| Size | Width | Use |
|---|---|---|
sm | 400px | Simple confirmations, ATOL/ABTA disclosure, small alerts |
md | 560px | Forms with 2–5 fields, standard enquiry modal |
lg | 720px | Complex forms, image galleries, multi-column content |
full | 100vw × 100vh | Mobile-first fullscreen modals, deep enquiry flows |
Default is md. Match the size to the content — a simple confirmation dialog should not be lg.
States
Closed
When open={false}, the modal renders null — no DOM nodes are created. This avoids hidden content that could be accidentally focussed.
Open
When open={true}:
- A full-viewport backdrop renders with
backdrop-blur-sm - The panel renders centred with
shadow-xl - Body scroll is locked (
overflow-hiddenondocument.body) - Focus moves to the first focusable element inside the modal
- Tab key cycles within the modal (focus trap active)
Submitting
When the user submits a form inside a modal, set the submit Button to loading={true}. The modal stays open during submission. Close it programmatically in the onSuccess callback.
Best practices
Always provide a title and a visible close affordance (close button or Cancel action in the footer).
A title and close button give the user two clear orientations: what it is, how to leave.
Hide the close button without providing another way to dismiss (e.g. a Cancel button in the footer).
A modal with no escape route feels like a trap — users will close the tab instead.
Use specific language in confirmation modals: 'Cancel booking KT-2026-0047?' not 'Are you sure?'
Confirmation modals should name the specific action and its consequence.
Use generic prompts like 'Are you sure?' or 'This will delete it.'
Vague confirmation text makes users hesitate — they cannot confirm they understand the consequence.
Size the modal to fit its content with minimal scrolling inside the panel.
Form modal fits comfortably in md (560px) — users can see the full form without scrolling.
Open a modal with so much body content that the user must scroll inside the panel significantly.
Scrolling inside a modal within a scrollable page disorients users.
Open modals only in direct response to a user action (button click, link tap).
The user triggered the modal by clicking a button — they understand the context.
Open a modal automatically on page load or after a timer.
Modals that fire on page load are experienced as popups — they damage trust.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| open* | boolean | — | Controls modal visibility. When false, the component returns null (no DOM nodes rendered). |
| onClose* | () => void | — | Called when the user clicks the backdrop, presses Escape, or clicks the close button. The parent controls the open state. |
| title | string | — | Modal header title. Used as the accessible name via aria-labelledby. |
| description | string | — | Optional subtitle below the title. Linked to the panel via aria-describedby. |
| size | 'sm' | 'md' | 'lg' | 'full' | 'md' | Panel width. sm=400px, md=560px, lg=720px, full=full-screen. |
| hideCloseButton | boolean | false | Hides the X button in the header. Only use when the footer provides a clear dismiss action. |
| children* | ReactNode | — | Modal body content. Can be any JSX — text, forms, images. |
| footer | ReactNode | — | Slot for action buttons. Rendered in a bordered footer area below the body. Typically contains Button components. |
Accessibility
Dialog role and ARIA
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
>
<h2 id="modal-title">Cancel this booking?</h2>
<p id="modal-desc">This action cannot be undone.</p>
...
</div>
role="dialog"announces the context to screen readers ("dialog" or "alert dialog")aria-modal="true"tells screen readers to hide everything outside the modalaria-labelledbypoints to the title element — screen readers read the title when focus enters the modalaria-describedbypoints to the description — screen readers read it after the title
Focus management
When open becomes true:
- Focus moves to the first focusable element inside the modal (usually the close button or first form field)
- Tab key cycles through focusable elements inside the modal — focus cannot escape
When open becomes false:
- Focus returns to the element that triggered the modal (the button that was clicked)
- This is handled automatically if you store a ref to the trigger
const triggerRef = useRef(null);
// When modal closes, return focus to trigger
useEffect(() => {
if (!open && triggerRef.current) {
triggerRef.current.focus();
}
}, [open]);
<Button ref={triggerRef} onClick={() => setOpen(true)}>
Enquire
</Button>
Keyboard interaction
| Key | Action |
|---|---|
Escape | Closes the modal (calls onClose) |
Tab | Moves focus to next focusable element (wraps within modal) |
Shift + Tab | Moves focus to previous focusable element (wraps within modal) |
Enter / Space | Activates focused button |
Body scroll lock
When the modal opens, overflow-hidden is applied to document.body to prevent background content from scrolling while the modal is open. This is removed when the modal closes.
Skip-nav consideration
If your page has a skip navigation link, ensure it targets content inside the modal when the modal is open — or that the skip link is not reachable while the modal's focus trap is active.