Gecko UIGecko UI

Drawer

Slide-out panel component that displays content from any edge of the viewport

Drawer

A slide-out panel component that displays auxiliary content from any edge of the viewport. Provides a less intrusive alternative to modals for navigation menus, settings panels, filters, and contextual information with smooth transitions and flexible dismissal behaviors.

Supports both controlled JSX usage and an imperative Drawer.show() / Drawer.dismiss() API. Multiple imperative drawers can be stacked.

Setup

The imperative API requires <GeckoUIProvider> to wrap your app.

import { Drawer } from '@geckoui/geckoui';

Basic Usage

import { useState } from 'react';
import { Drawer, Button } from '@geckoui/geckoui';

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

  return (
    <>
      <Button onClick={() => setOpen(true)}>Open Drawer</Button>
      <Drawer
        open={open}
        onClose={() => setOpen(false)}
        placement="right"
        allowClickOutside
        className="w-80 p-6"
      >
        <h2 className="text-xl font-semibold mb-4">Drawer Content</h2>
        <p className="text-gray-600 mb-4">This drawer slides in from the right side.</p>
        <Button onClick={() => setOpen(false)}>Close</Button>
      </Drawer>
    </>
  );
}

Props API

PropTypeDefaultDescription
openbooleanfalseControl drawer visibility
onClose() => void—Callback when drawer should close
placement'top' | 'right' | 'bottom' | 'left''right'Edge from which drawer slides
allowClickOutsidebooleanfalseMake the backdrop click-through, so clicking anywhere outside the drawer closes it
dismissOnEscapebooleantrueClose drawer on Escape key press
hideBackdropbooleanfalseHide backdrop overlay (visual only)
keepMountedbooleanfalseKeep the children in the DOM while the drawer is closed
backdropClassNamestring—CSS class for backdrop
classNamestring—CSS class for drawer panel
childrenReactNode—Drawer content
styleCSSProperties—Inline styles for the drawer root. GeckoUIProvider uses it to stack drawers opened with Drawer.show()

Backdrop click

  • allowClickOutside: false (default) — the backdrop blocks clicks, and clicking it calls onClose.
  • allowClickOutside: true — the backdrop becomes click-through, so clicking anywhere outside the panel calls onClose and the click also reaches the page underneath.

Closed content

Children render while the drawer is open and through the slide out, then unmount. A form inside starts fresh on every open, and an autoFocus field only takes focus when the drawer is shown, so there is nothing to reset by hand.

Pass keepMounted when state should survive closing, such as a half filled form the user may come back to:

<Drawer open={open} onClose={() => setOpen(false)} keepMounted>
  <DraftForm />
</Drawer>

Examples

Mobile Navigation Menu

const [menuOpen, setMenuOpen] = useState(false);

<Drawer open={menuOpen} onClose={() => setMenuOpen(false)} placement="left" allowClickOutside className="w-80">
  <nav className="p-6">
    <h2 className="text-lg font-semibold mb-4">Navigation</h2>
    <ul className="space-y-2">
      <li><a href="#" className="block py-2 px-3 rounded hover:bg-gray-100">Home</a></li>
      <li><a href="#" className="block py-2 px-3 rounded hover:bg-gray-100">Products</a></li>
    </ul>
  </nav>
</Drawer>

Filter Panel

<Drawer open={filtersOpen} onClose={() => setFiltersOpen(false)} placement="right" allowClickOutside backdropClassName="bg-black/60" className="w-96 p-6">
  <h2 className="text-xl font-semibold mb-4">Filters</h2>
  {/* filter controls */}
</Drawer>

Bottom Sheet

<Drawer open={isOpen} onClose={() => setOpen(false)} placement="bottom" allowClickOutside backdropClassName="bg-black/40" className="h-64 rounded-t-2xl">
  <div className="p-6">
    <h3 className="text-lg font-semibold mb-4">Quick Actions</h3>
  </div>
</Drawer>

Without Backdrop

<Drawer open={settingsVisible} onClose={() => setSettingsVisible(false)} placement="right" hideBackdrop allowClickOutside={false} className="w-[600px] border-l">
  <div className="p-6">Settings panel</div>
</Drawer>

Imperative API

Drawer.show() pushes a new drawer onto the overlay stack and returns an id. Call Drawer.dismiss(id) to close a specific drawer, or Drawer.dismiss() for the topmost drawer. It never closes a dialog — use Dialog.dismiss() for those.

A onClose passed in the options is still called when the drawer is dismissed, so you can use it for cleanup or tracking.

const id = Drawer.show(
  <QuickActionsMenu
    actions={globalActions}
    onActionClick={(action) => {
      handleAction(action);
      Drawer.dismiss(id);
    }}
  />,
  {
    placement: "bottom",
    className: "h-80 rounded-t-xl"
  }
);

Stacking

Drawer.show(<FirstPanel />, { placement: "right" });
// Later:
Drawer.show(<SecondPanel />, { placement: "right" }); // stacks on top

The topmost drawer captures Esc and click-outside events; lower drawers ignore them until they become topmost. Dialogs always render above drawers, so an open dialog is topmost even if a drawer was opened after it.

Context consumption

Drawer.show(<UserProfilePanel />, { placement: "right" });
// UserProfilePanel can call useContext(AuthContext) and get the real value

Placement Options

  • "right" — Slides in from the right (default)
  • "left" — Slides in from the left
  • "top" — Slides down from the top
  • "bottom" — Slides up from the bottom

Styling

The drawer root (GeckoUIDrawer) sits at z-index: 1000. Drawers opened with Drawer.show() get an explicit 1000 + position. Dialogs start at 2000, so they always render above drawers.

Backdrop (GeckoUIDrawer__backdrop):

  • data-state="visible" | "hidden" — Backdrop visibility
  • data-clickthrough — Present when click-through is enabled

Drawer panel (GeckoUIDrawer__drawer):

  • data-placement="right" | "left" | "top" | "bottom" — Slide direction
  • data-state="open" | "closed" — Open/closed state

Accessibility

  • role="dialog" on the drawer panel, with aria-modal set when the backdrop is not click-through
  • A closed panel is inert, so nothing in it can be focused or reached by a screen reader, even with keepMounted
  • Keyboard navigation with Escape key support
  • Click-outside detection
  • A menu, popover or picker open inside it closes first: an outside click or Escape reaches only the topmost overlay, so the next one closes the drawer

Page scroll is locked while a drawer is open, unless allowClickOutside is set, since that drawer deliberately leaves the page usable. The scrollbar width is paid back as padding so nothing shifts sideways.

Not handled yet: there is no focus trap, and focus is not restored to the trigger on close. iOS Safari ignores overflow: hidden on the body, so the page behind can still be dragged there.

  • GeckoUIProvider — Required provider for the imperative API
  • Dialog — Centered modal dialogs
  • Menu — Dropdown menus and action lists