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

PropTypeDefaultDescription
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.
overlaybooleantrueDim the page behind the drawer.
dismissOnOverlaybooleantrueClose when the backdrop is clicked.
trapFocusbooleantrueKeep Tab focus inside the drawer while it is open.
titlestring—Accessible name of the dialog (visually hidden).
descriptionstring—Accessible description of the dialog (visually hidden).
aria-labelstring—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.
containerHTMLElement | 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

PropTypeDefaultDescription
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

PropTypeDefaultDescription
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 open
  • Escape key closes the drawer
  • Accessible 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

KeyActionWhen
EscapeCloses the drawerdrawer is open
TabCycles focus within the drawer panel—
Shift+TabCycles 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>
    </>
  );
}

Related