KKUONIDesign System

Components

Modal

Stable

Modals 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.

  1. 1
    BackdropSemi-transparent black overlay covering the full viewport (bg-black/50 + backdrop-blur-sm). Clicking the backdrop calls onClose.
  2. 2
    PanelWhite rounded-lg container, centred on screen. Width set by the size prop. Shadow-xl elevates it above the backdrop.
  3. 3
    HeaderContains the title (h2) and the close button (X icon). Separated from the body by a border-bottom.
  4. 4
    Close buttonX icon, top-right of header. Calls onClose. Can be hidden via hideCloseButton (rare — always provide another close affordance).
  5. 5
    DescriptionOptional subtitle below the title, above the body. Linked to the panel via aria-describedby.
  6. 6
    BodyScrollable content area. Accepts any ReactNode as children.
  7. 7
    FooterOptional 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.

Your booking reference is KT-2026-0047.

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.

Sara

Sizes

SizeWidthUse
sm400pxSimple confirmations, ATOL/ABTA disclosure, small alerts
md560pxForms with 2–5 fields, standard enquiry modal
lg720pxComplex forms, image galleries, multi-column content
full100vw × 100vhMobile-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}:

  1. A full-viewport backdrop renders with backdrop-blur-sm
  2. The panel renders centred with shadow-xl
  3. Body scroll is locked (overflow-hidden on document.body)
  4. Focus moves to the first focusable element inside the modal
  5. 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

Do

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.

Don't

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.

Do

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.

Don't

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.

Do

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.

Don't

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.

Do

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.

Don't

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

PropTypeDefaultDescription
open*booleanControls modal visibility. When false, the component returns null (no DOM nodes rendered).
onClose*() => voidCalled when the user clicks the backdrop, presses Escape, or clicks the close button. The parent controls the open state.
titlestringModal header title. Used as the accessible name via aria-labelledby.
descriptionstringOptional 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.
hideCloseButtonbooleanfalseHides the X button in the header. Only use when the footer provides a clear dismiss action.
children*ReactNodeModal body content. Can be any JSX — text, forms, images.
footerReactNodeSlot 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 modal
  • aria-labelledby points to the title element — screen readers read the title when focus enters the modal
  • aria-describedby points to the description — screen readers read it after the title

Focus management

When open becomes true:

  1. Focus moves to the first focusable element inside the modal (usually the close button or first form field)
  2. Tab key cycles through focusable elements inside the modal — focus cannot escape

When open becomes false:

  1. Focus returns to the element that triggered the modal (the button that was clicked)
  2. 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

KeyAction
EscapeCloses the modal (calls onClose)
TabMoves focus to next focusable element (wraps within modal)
Shift + TabMoves focus to previous focusable element (wraps within modal)
Enter / SpaceActivates 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.

  • Drawer — a better fit for longer or navigation-adjacent content that should stay anchored to a screen edge instead of centred and blocking.
  • Popover — a lighter-weight, non-blocking alternative for short contextual content anchored to a trigger element.