Standard Overlays
Controlled and uncontrolled overlays
Every overlay listed below accepts the same three props — open, defaultOpen, onOpenChange — so you can drive it from your own
application state, or hand it off and let it manage itself. The pattern is identical across
all of them; learn it once here instead of re-deriving it from each component's source.
The one rule
Pass open and you own the state. Omit it and the component owns the
state. There's no partial mode — a component checks whether open is
undefined and picks one or the other for its whole lifetime. Don't pass
open on some renders and omit it on others; pick a mode per usage site and
stay in it.
Uncontrolled (the default)
Omit open entirely and the overlay manages its own open/closed state
internally — clicking its trigger, pressing Escape, clicking the backdrop and clicking its
own close button all just work with zero wiring. Use defaultOpen to start it
open instead of closed.
// Uncontrolled — the component manages its own state
<ModalDialog trigger={<button>Edit profile</button>}>
<p>Form goes here.</p>
</ModalDialog>
// Start it open on mount, then let it manage itself from there
<SlideOverDrawer defaultOpen trigger={<button>Filters</button>}>
<FilterForm />
</SlideOverDrawer> Controlled
Pass open and the overlay never changes its own state again — you flip it,
typically from a useState in the parent. This is what lets a drawer be
triggered from a row in a table, a keyboard shortcut, a deep link, or any other place that
isn't the overlay's own trigger element.
const [open, setOpen] = useState(false);
<SlideOverDrawer open={open} onOpenChange={setOpen}>
<EditForm onSaved={() => setOpen(false)} />
</SlideOverDrawer>
// Open it from anywhere else in the app
<button onClick={() => setOpen(true)}>Edit from the table row</button> The mistake people actually make
onOpenChange fires on every internally-initiated change —
trigger click, Escape, clicking the backdrop, clicking the overlay's own close button.
In controlled mode the overlay doesn't close itself for any of those; it calls onOpenChange(false) and waits for the parent to actually flip open. Pass open without handling onOpenChange and
the overlay will visibly refuse to close — Escape, backdrop, close button, all of it stop
working, because nothing is listening.
// Wrong — open is controlled, but nothing updates it.
// Escape / backdrop / close button all do nothing.
<ModalDialog open={true}>...</ModalDialog>
// Right — onOpenChange is wired back into the state that drives open
<ModalDialog open={open} onOpenChange={setOpen}>...</ModalDialog> ContextMenu's extra prop: position
ContextMenu is the one component in this list with a prop beyond the shared
three. A right-click normally resolves its own coordinates from the click event — but a
controlled open with no coordinates is only half an API, because the menu still has to
render somewhere. Pass position when you're opening the menu
programmatically (a keyboard shortcut, an action button) rather than from a native
right-click, so the menu knows where to land.
const [open, setOpen] = useState(false);
const [position, setPosition] = useState({ x: 0, y: 0 });
function openFromButton(e: React.MouseEvent) {
const rect = e.currentTarget.getBoundingClientRect();
setPosition({ x: rect.left, y: rect.bottom });
setOpen(true);
}
<ContextMenu open={open} onOpenChange={setOpen} position={position}>
<MenuItem onSelect={...}>Rename</MenuItem>
<MenuItem onSelect={...}>Delete</MenuItem>
</ContextMenu>
Omit position and a controlled ContextMenu still resolves its
own coordinates from whatever triggered it — the prop only matters once you're opening it
from somewhere that isn't a right-click.
Which components have it
18 of the 25 published components in Standard Overlays & Menus carry this API. The
rest in that category — Anchored Callout Badge, Coach-Mark Tour Step, Inline Notification
Banner, Stacked Sheet Layers, Toast Stack, Tooltip and Undo Snackbar — don't take open/defaultOpen/onOpenChange, mostly because
they're driven by a timer, a queue, or a hover delay rather than a single open/closed
flag. Check the component's own reference page before assuming it's controllable.
- Alert Dialog
- Bottom Sheet (Snap Points)
- Context Menu
- Date Range Popover
- Destructive Confirm Dialog
- Dropdown Action Menu
- Hover Card
- Lightbox Gallery Overlay
- Mobile Filter Sheet
- Modal Dialog
- Multi-Step Dialog
- Nested Submenu Menu
- Popover with Arrow
- Searchable Select Menu
- Share Sheet Overlay
- Sheet With Tabs
- Slide-Over Drawer
- Split Button Menu