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
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Control drawer visibility |
onClose | () => void | — | Callback when drawer should close |
placement | 'top' | 'right' | 'bottom' | 'left' | 'right' | Edge from which drawer slides |
allowClickOutside | boolean | false | Make the backdrop click-through, so clicking anywhere outside the drawer closes it |
dismissOnEscape | boolean | true | Close drawer on Escape key press |
hideBackdrop | boolean | false | Hide backdrop overlay (visual only) |
keepMounted | boolean | false | Keep the children in the DOM while the drawer is closed |
backdropClassName | string | — | CSS class for backdrop |
className | string | — | CSS class for drawer panel |
children | ReactNode | — | Drawer content |
style | CSSProperties | — | 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 callsonClose.allowClickOutside: true— the backdrop becomes click-through, so clicking anywhere outside the panel callsonCloseand 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 topThe 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 valuePlacement 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 visibilitydata-clickthrough— Present when click-through is enabled
Drawer panel (GeckoUIDrawer__drawer):
data-placement="right" | "left" | "top" | "bottom"— Slide directiondata-state="open" | "closed"— Open/closed state
Accessibility
role="dialog"on the drawer panel, witharia-modalset 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 withkeepMounted - 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.
Related Components
- GeckoUIProvider — Required provider for the imperative API
- Dialog — Centered modal dialogs
- Menu — Dropdown menus and action lists