KKUONIDesign System

Components

Drawer

Beta

A slide-in panel that appears from the right, left, or bottom of the viewport. Used for contextual tasks such as filters, detail views, and settings — without navigating away from the current page.

Filter results
Filter options here...
Apply filters

Purpose

The Drawer is a sliding panel that overlays the current page from one edge of the viewport. It is designed for tasks that are contextually related to the page but require enough space to warrant a dedicated panel — filtering search results, viewing booking details, adjusting preferences, or completing a short sub-task.

Unlike a Modal, the Drawer does not demand full attention before the user can continue. The backdrop is present but the underlying page remains partially visible, signalling that the user can return to it easily.

Usage

Use a Drawer for:

  • Filter panels on search results and destination listing pages
  • Booking detail side panels triggered from an itinerary list
  • Settings and preference panels (e.g. currency, language)
  • Mobile navigation menus (use side='left') — this is how MegaMenu implements its mobile nav
  • Quick-add or edit forms that do not require a full page

Do not use a Drawer for:

  • Critical confirmations that must block all interaction — use Modal instead
  • Content that could live inline on the page without disrupting the user's flow
  • Full multi-step journeys — those belong on dedicated pages
  • Content above 480px wide on mobile — consider a bottom-sheet (side='bottom') instead

Anatomy

Filter results
Apply filters
  1. 1
    BackdropSemi-transparent black overlay (bg-black/50) behind the panel. Clicking it calls onClose when closeOnBackdrop is true (the default).
  2. 2
    PanelWhite slide-in container. Width set by the width prop when side is right or left. Height fills the viewport. Rendered into document.body via createPortal.
  3. 3
    HeaderContains the title and the close (X) button. Always visible, not scrollable. Separated from the body by a border-bottom.
  4. 4
    Close buttonX icon in the header, top-right. Calls onClose. Always present — the Drawer does not support hiding the close button.
  5. 5
    BodyScrollable content area that fills available height between header and footer. Accepts any ReactNode as children.
  6. 6
    FooterOptional sticky area at the bottom of the panel, above a border-top. Typically contains primary and secondary action buttons.

Variants

Right drawer (default)

Slides in from the right. The most common configuration — used for filter panels and detail views on desktop.

const [open, setOpen] = useState(false);

<button onClick={() => setOpen(true)}>Open filters</button>
<Drawer
  open={open}
  onClose={() => setOpen(false)}
  title="Filter results"
  side="right"
  footer={
    <>
      <Button variant="primary" fullWidth>Apply filters</Button>
      <Button variant="ghost" fullWidth onClick={() => setOpen(false)}>Clear all</Button>
    </>
  }
>
  <FilterForm />
</Drawer>
Filter results
Apply filters
Clear all

Left drawer

Slides in from the left. Commonly used for mobile navigation menus or site-level settings panels.

<Drawer
  open={open}
  onClose={() => setOpen(false)}
  title="Menu"
  side="left"
  width="sm"
>
  <nav>
    <a href="/destinations">Destinations</a>
    <a href="/holiday-types">Holiday Types</a>
    <a href="/about">About Kuoni</a>
  </nav>
</Drawer>
Menu
Destinations
Holiday Types
About Kuoni

Bottom drawer

Slides up from the bottom edge. Best suited for mobile contexts where a side panel would be too narrow, or for quick-action sheets.

<Drawer
  open={open}
  onClose={() => setOpen(false)}
  title="Sort by"
  side="bottom"
>
  <ul>
    <li>Price: low to high</li>
    <li>Price: high to low</li>
    <li>Most popular</li>
  </ul>
</Drawer>
Sort by
Price: low to high
Price: high to low
Most popular

States

Closed

When open={false}, the Drawer and its backdrop do not render. The portal is unmounted. No content is present in the DOM.

Opening

When open transitions from false to true, the panel animates in via a CSS translate transition (300ms ease-in-out). The backdrop fades in simultaneously. Focus moves to the first focusable element inside the panel.

Open

The panel is fully visible. The backdrop is interactive. Body scroll is locked. Focus is trapped inside the panel. Pressing Escape calls onClose.

Closing

When open transitions to false, the panel animates out. After the transition completes, the portal is unmounted and focus returns to the element that triggered the Drawer.

Width sizes

SizeWidthTypical use
sm288pxMobile navigation, simple action sheets
md320pxFilter panels, short forms (default)
lg384pxDetail views, longer forms
xl480pxRich booking detail panels
full100%Full-screen mobile overlays

The width prop applies only when side is 'right' or 'left'. When side='bottom', the panel spans the full viewport width.

Best practices

Do

Use a Drawer for contextual tasks (filtering, viewing details) where the user benefits from seeing the page behind the panel.

A drawer keeps the user oriented — they can see the page behind it and return to it by closing.

Don't

Use a Drawer as a substitute for a Modal when you need the user to complete a required action before continuing.

A Drawer over a fully obscured page is a Modal with worse semantics and a harder dismiss target.

Do

Place primary actions (Apply, Save, Confirm) in the footer prop so they remain visible without scrolling.

Putting the primary action in the sticky footer means it is always visible, even when the body scrolls.

Don't

Embed the primary action button inside the children (body) of a Drawer that has scrollable content.

A CTA at the bottom of a long scrollable body is easy to miss and requires the user to scroll down to find it.

Do

Use side='bottom' for action sheets and sort/filter pickers on mobile viewports.

bottom provides a familiar sheet pattern that feels native on touch devices.

Don't

Use side='right' with width='xl' or 'full' on mobile viewports — switch to side='bottom' instead.

A right-side panel on a 375px screen leaves the content area too narrow to be usable.

Do

Store a ref to the trigger element and return focus to it when the Drawer closes.

Returning focus to the trigger gives keyboard and screen reader users a clear sense of where they were.

Don't

Let focus fall to the document body or the top of the page when the Drawer closes.

Focus dropping to the top of the document disorients keyboard users who opened the Drawer from a button mid-page.

Props

PropTypeDefaultDescription
open*booleanControls whether the Drawer is visible. When false, the panel and backdrop are unmounted. The parent owns this state.
onClose*() => voidCalled when the user presses Escape, clicks the close button, or clicks the backdrop (if closeOnBackdrop is true). The parent is responsible for setting open to false.
side'right' | 'left' | 'bottom''right'The edge of the viewport the panel slides in from.
titlestringText shown in the Drawer header. Used as the accessible name via aria-labelledby.
children*ReactNodeThe scrollable body content of the Drawer.
footerReactNodeOptional content rendered in a sticky footer area above the bottom border. Typically contains Button components.
width'sm' | 'md' | 'lg' | 'xl' | 'full''md'Panel width when side is right or left. sm=288px, md=320px, lg=384px, xl=480px, full=100%. Has no effect when side=bottom.
closeOnBackdropbooleantrueWhen true, clicking the backdrop calls onClose. Set to false when the user must complete an action inside the Drawer before dismissing.
classNamestringAdditional CSS classes applied to the panel element (not the backdrop).

Accessibility

Dialog role and ARIA

The Drawer panel uses role="dialog" with aria-modal="true" and is labelled by its title:

<div
  role="dialog"
  aria-modal="true"
  aria-labelledby="drawer-title"
>
  <h2 id="drawer-title">Filter results</h2>
  ...
</div>
  • role="dialog" announces the panel to screen readers as a dialog
  • aria-modal="true" instructs screen readers to restrict their virtual cursor to the panel's content
  • aria-labelledby connects the title element — screen readers announce the title when focus enters

Portal and document structure

The Drawer renders into document.body via createPortal. This means the panel appears outside the normal React tree in the DOM, which is intentional — it ensures the panel and backdrop always overlay all other content regardless of stacking context.

import { createPortal } from 'react-dom';

// Inside the Drawer component:
return createPortal(
  <div> {/* backdrop + panel */} </div>,
  document.body
);

Because of this, the Drawer cannot be rendered in a Node.js server environment (static build, SSR without a browser). Always guard with a typeof window !== 'undefined' check or use useEffect to mount the portal after hydration.

Focus management

When open becomes true:

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

When open becomes false:

  1. Focus returns to the element that triggered the Drawer
  2. Store a ref to the trigger to handle this reliably:
const triggerRef = useRef(null);

<button ref={triggerRef} onClick={() => setOpen(true)}>
  Open filters
</button>
<Drawer open={open} onClose={() => setOpen(false)} title="Filters">
  ...
</Drawer>

Body scroll lock

When the Drawer opens, overflow-hidden is applied to document.body. This prevents the background page from scrolling while the panel is active. The style is removed when the Drawer closes.

Keyboard interaction

KeyAction
EscapeCloses the Drawer (calls onClose)
TabMoves focus to the next focusable element (wraps within the panel)
Shift + TabMoves focus to the previous focusable element (wraps within the panel)
Enter / SpaceActivates the focused button or link
  • Modal — use instead of Drawer when the task must block all interaction with the page behind it.
  • MegaMenu — uses Drawer (side='left') for its mobile navigation pattern.