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">×</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
| On | Attribute | Values |
|---|---|---|
.corn-drawer-backdrop | data-visible | present or absent |
.corn-drawer | data-side | right|left |
.corn-drawer | data-open | present or absent |
.corn-drawer | data-size | sm|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
| Option | Type | Default | Description |
|---|---|---|---|
title | string | undefined | Drawer heading text |
content | string|HTMLElement | undefined | Body content. A string is shown as text, never parsed as HTML; pass an element for markup. |
footer | HTMLElement | undefined | Footer element |
side | 'left'|'right' | 'right' | Which side the drawer slides from |
size | 'sm'|'md'|'lg' | 'md' | Drawer width |
closeable | boolean | true | Allow backdrop/esc/close-button to dismiss |
onOpen | Function | undefined | Called when drawer opens |
onClose | Function | undefined | Called 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
| Prop | Type | Default |
|---|---|---|
open | boolean | required |
onClose | Function | required |
title | string | undefined |
side | 'left'|'right' | 'right' |
size | 'sm'|'md'|'lg' | 'md' |
closeable | boolean | true |
className | string | undefined |
children | React.ReactNode | required |
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:
- an element marked
data-autofocus— on a wrapper, the first control inside it - a field React’s
autoFocushas already focused - an element with a plain HTML
autofocusattribute - the first control in the content
- 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.