PixelSheet

Mobile-first bottom/top sheet with focus trap, scroll lock, Escape-to-close and optional drag handle.

  • Bottom or top anchored, four sizes (sm/md/lg/full)
  • Focus trap, scroll lock and Escape-to-close out of the box
  • Optional drag handle affordance for touch dismissal
  • WCAG 4.1.2 compliant: requires title or aria-label for accessible name
  • Surface-aware borders from the nearest PxlKitSurfaceProvider
Status
stable
Category
overlays
Since
v1.8.0

API

import { PixelSheet } from '@pxlkit/ui-kit';
PropTypeDefaultDescription
open (required)boolean—Whether the sheet is visible; set it from onOpenChange.
onOpenChange (required)(open: boolean) => void—Called with false when the sheet asks to close (Escape, the backdrop).
side'bottom' | 'top''bottom'Edge of the viewport the sheet is docked to.
size'sm' | 'md' | 'lg' | 'full''md'Height preset.
dragHandlebooleanfalseDraw a drag handle affordance (decorative).
surface'pixel' | 'linear'—Surface override; defaults to the nearest provider.
titlestring—Title shown at the top; it names the dialog.
descriptionstring—Text under the title, wired via aria-describedby.
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.
children (required)React.ReactNode—Body content.

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 sheet
  • 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. Drag handle is decorative (aria-hidden) and intended as a visual affordance only.

Keyboard

KeyActionWhen
EscapeCloses the sheetsheet is open
TabCycles focus within the sheet panel—
Shift+TabCycles focus backward within the sheet panel—

Usage

import { useState } from 'react';
import { PixelSheet } from '@pxlkit/ui-kit';

export function Default() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Open sheet
      </button>
      <PixelSheet
        open={open}
        onOpenChange={setOpen}
        title="Quick actions"
        description="Pick an action below"
      >
        <p>Sheet content goes here.</p>
        <button type="button" onClick={() => setOpen(false)}>
          Close
        </button>
      </PixelSheet>
    </>
  );
}

Examples

Default

import { useState } from 'react';
import { PixelSheet } from '@pxlkit/ui-kit';

export function Default() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Open sheet
      </button>
      <PixelSheet
        open={open}
        onOpenChange={setOpen}
        title="Quick actions"
        description="Pick an action below"
      >
        <p>Sheet content goes here.</p>
        <button type="button" onClick={() => setOpen(false)}>
          Close
        </button>
      </PixelSheet>
    </>
  );
}

With drag handle

import { useState } from 'react';
import { PixelSheet } from '@pxlkit/ui-kit';

export function WithDragHandle() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Open sheet
      </button>
      <PixelSheet
        open={open}
        onOpenChange={setOpen}
        size="lg"
        dragHandle
        title="Drag handle"
      >
        <p>Bottom sheet with a drag handle affordance.</p>
      </PixelSheet>
    </>
  );
}

Top side, full height

import { useState } from 'react';
import { PixelSheet } from '@pxlkit/ui-kit';

export function TopFull() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <button type="button" onClick={() => setOpen(true)}>
        Open top sheet
      </button>
      <PixelSheet
        open={open}
        onOpenChange={setOpen}
        side="top"
        size="full"
        aria-label="Top full-screen sheet"
      >
        <p>Top-anchored full-height sheet.</p>
        <button type="button" onClick={() => setOpen(false)}>
          Close
        </button>
      </PixelSheet>
    </>
  );
}

Related