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';
Prop
Type
Default
Description
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.
dragHandle
boolean
false
Draw a drag handle affordance (decorative).
surface
'pixel' | 'linear'
—
Surface override; defaults to the nearest provider.
title
string
—
Title shown at the top; it names the dialog.
description
string
—
Text under the title, wired via aria-describedby.
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.
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
Key
Action
When
Escape
Closes the sheet
sheet is open
Tab
Cycles focus within the sheet panel
—
Shift+Tab
Cycles 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>
</>
);
}