Centered modal dialog with title bar, optional description and footer, surface-aware chrome, focus trap, scroll lock, and async close support.
Five sizes (sm/md/lg/xl/full) with surface-aware chrome — pixel renders an old-school window, linear a flat card
Focus trap, scroll lock, and Escape-to-close come built in
Optional description wired via aria-describedby and optional footer slot for actions
asyncClose awaits a promise (with loading affordance on the close button) before unmounting
Portals to document.body by default; accepts a custom container override
Status
stable
Category
overlays
Since
v1.0.0
API
import { PixelModal } from '@pxlkit/ui-kit';
Prop
Type
Default
Description
open* (required)
boolean
—
Whether the modal is currently visible.
title* (required)
string
—
Modal title shown in the header.
children* (required)
React.ReactNode
—
Modal body content.
onClose* (required)
() => void
—
Called when the user requests to close the modal.
size
'sm' | 'md' | 'lg' | 'xl' | 'full'
'md'
Width preset. Default 'md'.
surface
'pixel' | 'linear'
—
Visual surface override. Falls back to nearest <PxlKitProvider> surface.
closeLabel
string
'Close'
Optional override for the close button's accessible label.
footer
React.ReactNode
—
Optional footer node, rendered at the bottom separated by a surface-aware divider.
description
React.ReactNode
—
Optional description, wired via aria-describedby for AT users.
asyncClose
() => Promise<void>
—
When provided, the close button awaits this promise (and shows a loading state) before the consumer-controlled onClose is invoked. Lets callers persist or animate-out before unmounting.
container
HTMLElement | null
—
Optional portal container override. Defaults to document.body.
ref points to <div>.
Accessibility
WCAG target: 2.1 AA
dialog
Renders with role="dialog" and aria-modal="true". Title is exposed via aria-labelledby; description (when provided) via aria-describedby. The close button advertises aria-busy while asyncClose is in flight. Backdrop click is equivalent to Escape and is disabled while closing to avoid double-firing.
Keyboard
Key
Action
When
Escape
Closes the modal (awaits asyncClose if provided)
—
Tab
Cycles focus within the trapped dialog
—
Shift+Tab
Cycles focus backwards within the trapped dialog
—
Usage
import { useState } from 'react';
import { PixelModal, PixelButton } from '@pxlkit/ui-kit';
export function Default() {
const [open, setOpen] = useState(false);
return (
<>
<PixelButton onClick={() => setOpen(true)}>Open modal</PixelButton>
<PixelModal open={open} onClose={() => setOpen(false)} title="Default modal">
<p>This is a minimal modal with title and body content.</p>
</PixelModal>
</>
);
}
Examples
Default
import { useState } from 'react';
import { PixelModal, PixelButton } from '@pxlkit/ui-kit';
export function Default() {
const [open, setOpen] = useState(false);
return (
<>
<PixelButton onClick={() => setOpen(true)}>Open modal</PixelButton>
<PixelModal open={open} onClose={() => setOpen(false)} title="Default modal">
<p>This is a minimal modal with title and body content.</p>
</PixelModal>
</>
);
}