DismissableLayer
A flexible utility component for building dismissable overlays, modals, and popovers with fine‑grained outside‑interaction control.
"use client";
DismissableLayer
DismissableLayer helps you build overlays (modals, drawers, popovers, menus) that should close when a user interacts outside of them or presses Escape. It provides fine‑grained hooks for pointer/focus interactions outside the layer, supports disabling outside pointer events, and composes well when layers are nested.
Use DismissableLayer whenever you need “click outside to close” or “press Escape to close” behavior. For regions inside a layer that must not trigger dismissal, wrap them with DismissableLayerBranch.
Features
- Dismiss on click/tap outside or on Escape
- Optional disabling of outside pointer events
- Customizable hooks for outside pointer and focus events
- Composable/nestable — multiple layers can coexist
- Branches that do not trigger dismissal
- Works with portals and layered UIs
API
DismissableLayer props
Prop
Type
DismissableLayerBranch props
Prop
Type
Use DismissableLayerBranch for nested interactive regions (e.g., a button inside a popover that should not dismiss the popover when clicked).
Usage
Basic
import * as React from 'react';
import { Button } from '@loke/design-system/button';
import { DismissableLayer } from '@loke/ui/dismissable-layer';
export function BasicExample() {
const [open, setOpen] = React.useState(false);
return (
<div className="relative h-[300px] w-full bg-muted p-4">
<Button onClick={() => setOpen(true)}>Open Layer</Button>
{open ? (
<DismissableLayer
className="absolute left-20 top-20 rounded-md border bg-card p-4 shadow-lg"
onDismiss={() => setOpen(false)}
>
<h2 className="mb-2 font-semibold text-lg">Dismissable Layer</h2>
<p className="text-sm text-muted-foreground">
Click outside or press Escape to dismiss.
</p>
</DismissableLayer>
) : null}
</div>
);
}Disable outside pointer events
export function DisabledOutsidePointerEventsExample() {
const [open, setOpen] = React.useState(false);
return (
<div className="relative h-[300px] w-full bg-muted p-4">
<Button onClick={() => setOpen(true)}>Open Layer</Button>
{open ? (
<DismissableLayer
disableOutsidePointerEvents
className="absolute left-20 top-20 rounded-md border bg-card p-4 shadow-lg"
onDismiss={() => setOpen(false)}
>
<h2 className="mb-2 font-semibold text-lg">
Outside pointer events are disabled
</h2>
<p className="text-sm text-muted-foreground">
Clicking outside will dismiss this layer, and outside elements will not
receive pointer events until it’s closed.
</p>
</DismissableLayer>
) : null}
</div>
);
}With a Branch (non‑dismissable region)
export function WithBranchExample() {
const [open, setOpen] = React.useState(false);
return (
<div className="relative h-[300px] w-full bg-muted p-4">
<Button onClick={() => setOpen(true)}>Open Layer</Button>
{open ? (
<DismissableLayer
className="absolute left-20 top-20 rounded-md border bg-card p-4 shadow-lg"
onDismiss={() => setOpen(false)}
>
<h2 className="mb-2 font-semibold text-lg">Layer with Branch</h2>
<p className="text-sm text-muted-foreground">
Actions inside the Branch won’t dismiss the layer.
</p>
<DismissableLayerBranch className="mt-4">
<Button onClick={() => alert('Branch clicked')}>Click me</Button>
</DismissableLayerBranch>
</DismissableLayer>
) : null}
</div>
);
}Nested layers
export function NestedLayersExample() {
const [outerOpen, setOuterOpen] = React.useState(false);
const [innerOpen, setInnerOpen] = React.useState(false);
return (
<div className="relative h-[400px] w-full bg-muted p-4">
<Button onClick={() => setOuterOpen(true)}>Open Outer Layer</Button>
{outerOpen ? (
<DismissableLayer
className="absolute left-20 top-20 rounded-md border bg-card p-4 shadow-lg"
onDismiss={() => setOuterOpen(false)}
>
<h2 className="mb-2 font-semibold text-lg">Outer Layer</h2>
<p className="text-sm text-muted-foreground">
Click below to open an inner layer.
</p>
<Button className="mt-4" onClick={() => setInnerOpen(true)}>
Open Inner Layer
</Button>
{innerOpen ? (
<DismissableLayer
className="absolute left-10 top-10 rounded-md border bg-muted p-4 shadow-lg"
onDismiss={() => setInnerOpen(false)}
>
<h3 className="mb-2 font-semibold">Inner Layer</h3>
<p className="text-sm text-muted-foreground">
Interact outside to dismiss this layer first.
</p>
</DismissableLayer>
) : null}
</DismissableLayer>
) : null}
</div>
);
}Advanced handlers
You can customize dismissal logic by preventing the default on outside interactions:
<DismissableLayer
onPointerDownOutside={(event) => {
const original = event.detail.originalEvent;
// Example: don’t dismiss if click is on a specific element
if ((original.target as HTMLElement)?.closest('[data-keep-open]')) {
event.preventDefault();
}
}}
onInteractOutside={(event) => {
// Generic “outside interaction” handler — runs for pointer or focus events
// event.preventDefault() here will also prevent dismissal
}}
onDismiss={() => setOpen(false)}
/>- onPointerDownOutside and onFocusOutside receive a custom event whose detail.originalEvent contains the native event.
- Call event.preventDefault() to stop dismissal.
Accessibility
- Escape key handling: built‑in onEscapeKeyDown allows keyboard users to close layers.
- Focus management: pair with focus management utilities (e.g., focus trap and restore) for fully accessible modals/dialogs.
- Pointer event disabling: when disableOutsidePointerEvents is true, outside content won’t interact until the layer is closed — this can mirror modal behavior.
Best practices
- Keep actions that shouldn’t dismiss the layer inside a DismissableLayerBranch.
- Use disableOutsidePointerEvents for modal-like experiences.
- For accessible dialogs, combine DismissableLayer with proper focus trapping and ARIA labelling (e.g., role="dialog", aria-labelledby).
- When nesting, ensure the inner layer dismisses first — the component already accounts for layering order.
Focus Guards Utility
Inject invisible focus guards at the document edges so focus transitions can be captured consistently across your app (useful for modals, overlays, and complex focus management).
Component Guide
Overview of the key components available in the LOKE Design System with imports, use cases, and composition tips.