PixelDrawer
Side-anchored modal panel (right/left/top/bottom) with focus trap, scroll lock and Escape-to-close.
- Four anchor sides (right/left/top/bottom) and five sizes (sm/md/lg/xl/full)
- Focus trap, scroll lock and Escape-to-close out of the box
- WCAG 4.1.2 compliant: requires
title or aria-label for accessible name - Surface-aware borders from the nearest PxlKitSurfaceProvider
- Composable subparts: header, body and footer (
PixelDrawer.Header in React, PixelDrawerHeader in Vue, pxl-drawer-header in Angular, and so on)
- Status
- stable
- Category
- overlays
- Since
- v1.8.0
API
import { PixelDrawer } from '@pxlkit/ui-kit';
PixelDrawer
| Prop | Type | Default | Description |
|---|
open* (required) | boolean | — | Whether the drawer is visible; set it from onOpenChange. |
onOpenChange* (required) | (open: boolean) => void | — | Called with false when the drawer asks to close (Escape, the backdrop). |
side | 'right' | 'left' | 'top' | 'bottom' | 'right' | Edge of the viewport the drawer is anchored to. |
size | 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'md' | Width (left / right) or height (top / bottom) preset. |
overlay | boolean | true | Dim the page behind the drawer. |
dismissOnOverlay | boolean | true | Close when the backdrop is clicked. |
trapFocus | boolean | true | Keep Tab focus inside the drawer while it is open. |
title | string | — | Accessible name of the dialog (visually hidden). |
description | string | — | Accessible description of the dialog (visually hidden). |
aria-label | string | — | Accessible name fallback when title is omitted. WCAG 4.1.2 requires every role="dialog" to expose a name; supply title OR aria-label. |
surface | 'pixel' | 'linear' | — | Surface override; defaults to the nearest provider. |
container | HTMLElement | null | — | Portal target; document.body when left out. |
children* (required) | React.ReactNode | — | The drawer content — typically PixelDrawer.Header, PixelDrawer.Body and PixelDrawer.Footer. |
ref points to <div>.
PixelDrawer.Header
| Prop | Type | Default | Description |
|---|
surface | 'pixel' | 'linear' | — | Surface override; defaults to the nearest provider. |
Also takes the native attributes and event handlers of <div> (HTMLAttributes<HTMLDivElement>).
ref points to <div>.
PixelDrawer.Body
No props of its own.
Also takes the native attributes and event handlers of <div> (HTMLAttributes<HTMLDivElement>).
ref points to <div>.
PixelDrawer.Footer
| Prop | Type | Default | Description |
|---|
surface | 'pixel' | 'linear' | — | Surface override; defaults to the nearest provider. |
Also takes the native attributes and event handlers of <div> (HTMLAttributes<HTMLDivElement>).
ref points to <div>.
Accessibility
WCAG target: 2.1 AA
role="dialog" with aria-modal="true"Focus trap inside panel (WCAG 2.1.2 No Keyboard Trap)Scroll lock on body while openEscape key closes the drawerAccessible name via `title` (aria-labelledby) or `aria-label`
Dev-only warning fires when neither title nor aria-label is provided to enforce WCAG 4.1.2.
Keyboard
| Key | Action | When |
|---|
| Escape | Closes the drawer | drawer is open |
| Tab | Cycles focus within the drawer panel | — |
| Shift+Tab | Cycles focus backward within the drawer panel | — |
Usage
import { useState } from 'react';
import { PixelDrawer } from '@pxlkit/ui-kit';
export function Default() {
const [open, setOpen] = useState(false);
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Open drawer
</button>
<PixelDrawer open={open} onOpenChange={setOpen} title="Settings">
<PixelDrawer.Header>
<span>Settings</span>
<button type="button" onClick={() => setOpen(false)}>
Close
</button>
</PixelDrawer.Header>
<PixelDrawer.Body>
<p>Drawer content goes here.</p>
</PixelDrawer.Body>
<PixelDrawer.Footer>
<button type="button" onClick={() => setOpen(false)}>
Done
</button>
</PixelDrawer.Footer>
</PixelDrawer>
</>
);
}
Examples
Default
import { useState } from 'react';
import { PixelDrawer } from '@pxlkit/ui-kit';
export function Default() {
const [open, setOpen] = useState(false);
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Open drawer
</button>
<PixelDrawer open={open} onOpenChange={setOpen} title="Settings">
<PixelDrawer.Header>
<span>Settings</span>
<button type="button" onClick={() => setOpen(false)}>
Close
</button>
</PixelDrawer.Header>
<PixelDrawer.Body>
<p>Drawer content goes here.</p>
</PixelDrawer.Body>
<PixelDrawer.Footer>
<button type="button" onClick={() => setOpen(false)}>
Done
</button>
</PixelDrawer.Footer>
</PixelDrawer>
</>
);
}
Left side, large
import { useState } from 'react';
import { PixelDrawer } from '@pxlkit/ui-kit';
export function LeftSide() {
const [open, setOpen] = useState(false);
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Open left drawer
</button>
<PixelDrawer
open={open}
onOpenChange={setOpen}
side="left"
size="lg"
title="Navigation"
>
<PixelDrawer.Header>Navigation</PixelDrawer.Header>
<PixelDrawer.Body>
<p>Menu items here.</p>
</PixelDrawer.Body>
</PixelDrawer>
</>
);
}
Bottom sheet
import { useState } from 'react';
import { PixelDrawer } from '@pxlkit/ui-kit';
export function BottomSheet() {
const [open, setOpen] = useState(false);
return (
<>
<button type="button" onClick={() => setOpen(true)}>
Open bottom sheet
</button>
<PixelDrawer
open={open}
onOpenChange={setOpen}
side="bottom"
size="md"
title="Quick actions"
description="Pick an action below"
>
<PixelDrawer.Body>
<p>Sheet content.</p>
</PixelDrawer.Body>
<PixelDrawer.Footer>
<button type="button" onClick={() => setOpen(false)}>
Cancel
</button>
</PixelDrawer.Footer>
</PixelDrawer>
</>
);
}