Drawer
A modal panel that slides in from a screen edge, such as a bottom sheet or mobile navigation. It traps focus, locks page scrolling, and can be swiped away.
Code
import {Button, Drawer, Stack} from '@earendil-works/elanor-react';
export default function DrawerDemo() {
return (
<Drawer.Root>
<Drawer.Trigger render={<Button hierarchy="secondary" />}>Share project</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Viewport>
<Drawer.Popup>
<Drawer.Content>
<Stack gap="large" align="stretch">
<Drawer.Title>Share project</Drawer.Title>
<Drawer.Description>
Anyone with the link can view this project. Swipe down to dismiss.
</Drawer.Description>
<Stack direction="row" gap="small">
<Drawer.Close render={<Button />}>Copy link</Drawer.Close>
<Drawer.Close render={<Button hierarchy="secondary" />}>Cancel</Drawer.Close>
</Stack>
</Stack>
</Drawer.Content>
</Drawer.Popup>
</Drawer.Viewport>
</Drawer.Portal>
</Drawer.Root>
);
} Usage
import {Drawer} from '@earendil-works/elanor-react';
<Drawer.Root>
<Drawer.Trigger />
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Viewport>
<Drawer.Popup>
<Drawer.Content>
<Drawer.Title />
<Drawer.Description />
<Drawer.Close />
</Drawer.Content>
</Drawer.Popup>
</Drawer.Viewport>
</Drawer.Portal>
</Drawer.Root>;
Popup scrolls within the viewport. Content keeps text selectable with a mouse; add data-base-ui-swipe-ignore to controls that handle their own gestures.
When a bottom sheet contains form fields, wrap its Portal in VirtualKeyboardProvider inside Root. On touch devices, it keeps the focused field visible above the software keyboard.
Side panel
swipeDirection="left" or "right" turns the drawer into a side panel; the default, "down", is a bottom sheet. Directions are physical, so they don’t flip in right-to-left layouts.
Close the drawer when the user navigates, with actionsRef.current.close() or controlled open. Don’t hide an open drawer with CSS at a breakpoint; close it instead. Keep its open state separate from Sidebar’s collapsed.
Code
import {CloseIcon, MenuIcon} from '@earendil-works/elanor-icons-react';
import {
Drawer,
IconButton,
SideNavigation,
Stack,
type DrawerRootActions,
} from '@earendil-works/elanor-react';
import {useRef, useState} from 'react';
const destinations = ['Overview', 'Projects', 'Members', 'Settings'];
export default function DrawerSidePanel() {
const actionsRef = useRef<DrawerRootActions>(null);
const [current, setCurrent] = useState('Overview');
return (
<Drawer.Root swipeDirection="left" actionsRef={actionsRef}>
<Drawer.Trigger render={<IconButton aria-label="Open navigation" />}>
<MenuIcon />
</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Viewport>
<Drawer.Popup>
<Drawer.Content>
<Stack gap="large" align="stretch">
<Stack direction="row" align="center" gap="auto">
<Drawer.Title>Acme workspace</Drawer.Title>
<Drawer.Close render={<IconButton aria-label="Close navigation" />}>
<CloseIcon />
</Drawer.Close>
</Stack>
<SideNavigation.Root aria-label="Workspace">
<SideNavigation.List>
{destinations.map((destination) => (
<SideNavigation.Item key={destination}>
<SideNavigation.Link
href="#"
active={destination === current}
onClick={(event) => {
event.preventDefault();
setCurrent(destination);
actionsRef.current?.close();
}}
>
{destination}
</SideNavigation.Link>
</SideNavigation.Item>
))}
</SideNavigation.List>
</SideNavigation.Root>
</Stack>
</Drawer.Content>
</Drawer.Popup>
</Drawer.Viewport>
</Drawer.Portal>
</Drawer.Root>
);
} Nested sheets
Nest a Drawer inside a Popup to stack bottom sheets. With data-stacked on each Popup, parent sheets shrink behind the open sheet. Closing a nested sheet returns focus to its trigger.
Code
import {Button, Drawer, Stack} from '@earendil-works/elanor-react';
export default function DrawerNested() {
return (
<Drawer.Root>
<Drawer.Trigger render={<Button hierarchy="secondary" />}>Project settings</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Viewport>
<Drawer.Popup data-stacked>
<Drawer.Content>
<Stack gap="large" align="stretch">
<Drawer.Title>Project settings</Drawer.Title>
<Drawer.Description>Open sharing settings in a nested sheet.</Drawer.Description>
<Drawer.Root>
<Drawer.Trigger render={<Button />}>Sharing settings</Drawer.Trigger>
<Drawer.Portal>
<Drawer.Backdrop />
<Drawer.Viewport>
<Drawer.Popup data-stacked>
<Drawer.Content>
<Stack gap="large" align="stretch">
<Drawer.Title>Sharing settings</Drawer.Title>
<Drawer.Description>
Swipe down or close this sheet to return to project settings.
</Drawer.Description>
<Drawer.Close render={<Button />}>Back</Drawer.Close>
</Stack>
</Drawer.Content>
</Drawer.Popup>
</Drawer.Viewport>
</Drawer.Portal>
</Drawer.Root>
<Drawer.Close render={<Button hierarchy="secondary" />}>Close</Drawer.Close>
</Stack>
</Drawer.Content>
</Drawer.Popup>
</Drawer.Viewport>
</Drawer.Portal>
</Drawer.Root>
);
} Overlay placement
The application decides how overlays stack. With an elevated header, give Drawer.Portal position: fixed, inset: 0, and z-index: var(--semantic-number-layer-overlay). Leave the backdrop, viewport, and nested popups on their default layers.
API
Drawer.Root
Accepts Base UI Drawer.Root props.
Drawer.Trigger
Accepts Base UI Drawer.Trigger props.
Drawer.Portal
Accepts Base UI Drawer.Portal props.
Drawer.Backdrop
Accepts Base UI Drawer.Backdrop props.
Drawer.Viewport
Accepts Base UI Drawer.Viewport props.
Drawer.Popup
Accepts Base UI Drawer.Popup props.
Drawer.Content
Accepts Base UI Drawer.Content props.
Drawer.Title
Accepts Base UI Drawer.Title props.
Drawer.Description
Accepts Base UI Drawer.Description props.
Drawer.Close
Accepts Base UI Drawer.Close props.
Drawer.VirtualKeyboardProvider
Accepts Base UI Drawer.VirtualKeyboardProvider props.