Drawer

A slide-in panel anchored to the left or right edge of the screen.

import { ... } from '@corndev/ui-overlays'

HTML

<div class="corn-drawer-backdrop" data-visible>
  <aside class="corn-drawer" data-side="right" data-size="md" data-open role="dialog">
    <div class="corn-drawer-header">
      <h2 class="corn-drawer-title">Filters</h2>
      <button class="corn-drawer-close" aria-label="Close">&times;</button>
    </div>
    <div class="corn-drawer-body">…</div>
  </aside>
</div>

Classes

Class
.corn-drawer
.corn-drawer-backdrop
.corn-drawer-header
.corn-drawer-title
.corn-drawer-close
.corn-drawer-body
.corn-drawer-footer
.corn-drawer-open

Attributes

OnAttributeValues
.corn-drawer-backdropdata-visiblepresent or absent
.corn-drawerdata-sideright|left
.corn-drawerdata-openpresent or absent
.corn-drawerdata-sizesm|md|lg

Vanilla JS

Required for behaviour — adds traps focus, makes the rest of the page inert, closes on Escape and backdrop click, and toggles the open classes.

import { createDrawer } from '@corndev/ui-overlays';
import '@corndev/ui/theme';

const instance = createDrawer({ /* options */ });

Options

OptionTypeDefaultDescription
titlestringundefinedDrawer heading text
contentstring|HTMLElementundefinedBody content. A string is shown as text, never parsed as HTML; pass an element for markup.
footerHTMLElementundefinedFooter element
side'left'|'right''right'Which side the drawer slides from
size'sm'|'md'|'lg''md'Drawer width
closeablebooleantrueAllow backdrop/esc/close-button to dismiss
onOpenFunctionundefinedCalled when drawer opens
onCloseFunctionundefinedCalled when drawer closes

React

Drawer, DrawerHeader, DrawerBody, DrawerFooter — from @corndev/ui-overlays-react.

Visibility is controlled by open; body and footer go in as Drawer.Body / Drawer.Footer children rather than as props. Drawer.Header takes children, onClose, and closeable.

Props

PropTypeDefault
openbooleanrequired
onCloseFunctionrequired
titlestringundefined
side'left'|'right''right'
size'sm'|'md'|'lg''md'
closeablebooleantrue
classNamestringundefined
childrenReact.ReactNoderequired

React usage

import { useState } from 'react';
import { Drawer } from '@corndev/ui-overlays-react';
import '@corndev/ui-overlays/styles';

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

<button onClick={() => setOpen(true)}>Open drawer</button>

<Drawer open={open} onClose={() => setOpen(false)} title="Settings">
  <Drawer.Body>
    <p>Drawer body content.</p>
  </Drawer.Body>
  <Drawer.Footer>
    <button onClick={() => setOpen(false)}>Close</button>
  </Drawer.Footer>
</Drawer>

Left side

<Drawer open={open} onClose={() => setOpen(false)} side="left" title="Navigation">
  <Drawer.Body>...</Drawer.Body>
</Drawer>

Sizes

<Drawer size="sm" ...>...</Drawer>
<Drawer size="md" ...>...</Drawer>
<Drawer size="lg" ...>...</Drawer>

Sub-components

<Drawer.Header>Custom header</Drawer.Header>
<Drawer.Body>Body content</Drawer.Body>
<Drawer.Footer>Footer content</Drawer.Footer>

What it does for accessibility

While a drawer is open the rest of the page is inert: it leaves the accessibility tree, stops responding to pointers and drops out of the focus order. A focus trap alone only holds the keyboard — a screen reader’s virtual cursor and touch exploration would still reach the page behind the panel.

A drawer opened over a modal makes that modal inert too, and closing the drawer gives it back. Anything you had already marked inert yourself stays that way after the drawer closes.

Where focus goes

When a drawer opens, focus goes to the first of these that exists:

  1. an element marked data-autofocus — on a wrapper, the first control inside it
  2. a field React’s autoFocus has already focused
  3. an element with a plain HTML autofocus attribute
  4. the first control in the content
  5. the close button, and only when there is nothing else

The close button is deliberately last. It renders first, in the header, so “the first focusable element” used to mean ✕ — which put the reader one Enter away from closing what they had just opened, and overrode any autoFocus a form asked for.

<Drawer open={open} onClose={close} title="Rename">
  <input data-autofocus defaultValue={name} />
</Drawer>

data-autofocus on an element, or on a wrapper around one, names it outright. If the content is lazy — a context modal whose chunk is still loading — focus moves onto it once it arrives, unless the reader has already moved focus themselves.

On close, focus goes back to whatever opened the drawer, however it closed.

Stacking

Escape closes only the drawer on top. Every open dialog listens for keys, so this matters: a “Discard changes?” confirm on top of a form must not take the form, and its unsaved work, with it. Each open dialog is also named by its own title, and the page stays scroll-locked until the last one closes.