Skip to content

Drawer

A panel attached to one edge of the window. It either floats over the page and is dismissed, or sits in the layout as a fixed sidebar: the same panel either way.

tsx
import { Button, Drawer } from 'neba';

<Drawer trigger={<Button variant="outline">Open navigation</Button>} title="Workspace">
  <List>…</List>
</Drawer>;

Props

PropTypeDefaultDescription
localestringBCP 47 tag naming the × in that language. Unsupported tags fall back to English
side'top' | 'right' | 'bottom' | 'left''left'Which edge of the window the panel is attached to. Physical, as NebaSide is everywhere
mode'overlay' | 'inline''overlay'overlay floats on a scrim, holds focus and is dismissed. inline is part of the layout: no scrim, no portal, nothing to dismiss
sizeshared'xs' | 'sm' | 'md' | 'lg' | 'xl''md'The type scale, the padding, and a side panel's default width
colorshared'primary' | 'secondary' | 'success' | 'warning' | 'danger' | 'info''primary'Semantic colour role. The sheet is never dyed, so it reaches the edge and the focus ring
densityshared'default' | 'compact''default'Padding only
openbooleanWhether it is shown. With onOpenChange, a controlled drawer. In inline mode it is whether the panel is in the layout
defaultOpenbooleanThe initial state of an uncontrolled drawer. false in overlay mode, true in inline mode: a fixed sidebar that had to be opened would not be fixed
onOpenChange(open: boolean) => voidCalled when it opens or closes
triggerReactElementThe element that opens it, wired up by Base UI. overlay only: an inline drawer has nothing to open, so it is not rendered
titleReactNodeThe heading. Rendered as the element that names the drawer
descriptionReactNodeA line under the title, and the drawer's accessible description
actionsReactNodeThe bottom row, held against the foot of the panel and end-aligned. DrawerClose is what makes one of them dismiss
dividersbooleanfalseSeparates the sections with hairlines instead of space. Worth turning on the moment the body scrolls
showClosebooleanThe × in the corner. On in overlay mode, off in inline mode: a × that closes a fixed sidebar with nothing to reopen it is a one-way door
closeLabelstringAccessible name of the × button
extentnumber | stringHow far the panel reaches in from its edge: a width for left and right, a height for top and bottom. Numbers are pixels
roundedbooleantrueCuts the two corners on the edge that faces the page. The corners against the window edge are always square
modalboolean | 'trap-focus'trueWhether the page behind is taken away. trap-focus keeps it scrollable and clickable while holding focus inside. overlay only
dismissiblebooleantrueWhether Escape or a click on the scrim closes it. overlay only
childrenReactNodeThe body: the only part that scrolls

Native <div> attributes pass through to the panel. Only color, title and children are excluded, since the table above spells them differently.

DrawerClose is Base UI's Dialog.Close, re-exported. Give it a render prop and any element dismisses the drawer it is inside: <DrawerClose render={<Button>Cancel</Button>} />. It belongs to an overlay drawer: an inline one is not a dialog.

The shared axes are described in prop conventions.

Examples

side

side is the edge the panel is attached to. left and right take a width from the size ladder and fill the height; top and bottom fill the width and are as tall as their content, up to 85% of the window.

mode

overlay, the default, is the drawer you open: a scrim, a focus trap, Escape, and focus returned to the trigger. inline puts the same panel in the layout (no scrim, no portal, nothing to dismiss), and open decides whether it is in the flow at all. It defaults to open, so a fixed sidebar needs no state.

Because it is one component, a sidebar that becomes a hamburger at a breakpoint is a mode that changes rather than two components to swap between.

rounded

rounded cuts the two corners on the edge facing the page: the top and bottom of a side panel, the inner pair of a top or bottom one. The corners against the window edge stay square. Turn it off for a panel that should read as an extension of the window.

dividers and scrolling

The body is the only part that scrolls, so title, description and actions stay put. dividers replaces the space between the sections with hairlines, which is what says the header did not move.

extent

extent is how far the panel reaches in from its edge: a width for left and right, a height for top and bottom. Numbers are pixels, strings are CSS lengths. Left alone, a side panel takes the width its size implies.

tsx
<Drawer side="right" extent={420} title="Details" />
<Drawer side="bottom" extent="50vh" title="Filters" />

Accessibility

  • In overlay mode the panel is a modal dialog: focus is trapped inside, the page behind goes inert, Escape closes it and focus returns to the trigger.
  • title names the drawer and description describes it, wired with aria-labelledby and aria-describedby. A drawer with neither needs an aria-label of its own.
  • modal="trap-focus" keeps the page scrollable and clickable while still holding focus inside.
  • dismissible={false} cancels Escape and the click on the scrim. Give that drawer actions that close it, because there will be no other way out.
  • An inline drawer is not a dialog: it takes no focus, traps nothing, and announces nothing. Its title is a plain heading, so put it in the page's heading order.
  • locale decides the ×'s accessible name; closeLabel writes it out instead.

Released under the MIT License