# pushLayer A dismiss layer: Escape and an outside press close an overlay, with a stack so only the top layer reacts. A layer pushed while another is open sits above it. Escape goes to the top layer only, so closing a menu inside a dialog leaves the dialog open and the next Escape closes the dialog; an outside press goes to the top layer and then down the stack to every layer it is also outside of, so a press outside nested popovers closes them all. ```ts Signatures function pushLayer(el: HTMLElement, options: LayerOptions): Layer; function layerCount(): number; function topLayer(): Layer | undefined; function onScrollbar(e: PointerEvent): boolean; type DismissReason = "escape" | "outside" | "focus-outside"; ``` ## Options | Option | Type | Default | What it does | |---|---|---|---| | `onDismiss` | `(reason: DismissReason, event: Event) => void` | required | Called when the layer should close. | | `escape` | `boolean \| (() => boolean)` | `true` | Escape dismisses. A function is read at the moment of the key press. | | `outside` | `boolean \| ((event: PointerEvent \| MouseEvent) => boolean)` | `true` | A pointer press outside dismisses. A function gets the event and returns whether it dismisses. | | `focusOutside` | `boolean \| (() => boolean)` | `false` | Focus moving outside dismisses, for a non-modal popover. | | `onEscapeKeyDown` | `(event: KeyboardEvent) => void` | none | Called before an Escape dismiss; `event.preventDefault()` keeps the layer open. | | `onPointerDownOutside` | `(event: PointerEvent) => void` | none | Called before an outside dismiss; `event.preventDefault()` keeps the layer open. | | `onFocusOutside` | `(event: FocusEvent) => void` | none | Called before a focus-outside dismiss; `event.preventDefault()` keeps the layer open. | | `onInteractOutside` | `(event: PointerEvent \| MouseEvent \| FocusEvent) => void` | none | Called before either outside dismiss, after the specific callback; `event.preventDefault()` keeps the layer open. | | `ancestorScroll` | `boolean` | `false` | Scrolling an element that contains an `inside()` element (the trigger) dismisses, with reason `"outside"` and the scroll event. | | `inside` | `() => (Element \| null \| undefined)[]` | none | Elements that count as inside the layer, such as its trigger. | | `blockOutside` | `boolean` | `false` | Blocks pointer interaction with everything below the layer while it is open, for a modal menu. The press that lands outside still dismisses. | | `bubbles` | `boolean \| { escapeKey?: boolean; outsidePress?: boolean }` | `{ escapeKey: false, outsidePress: true }` | How a dismiss travels to the layers below. `escapeKey: true` lets one Escape close a whole tree; `outsidePress: false` stops an outside press at this layer. A boolean sets both. | | `outsidePressEvent` | `"pointerdown" \| "mousedown" \| "click"` | `"pointerdown"` | Which event an outside mouse or pen press dismisses on. A touch always waits for the click. | | `capture` | `boolean \| { escapeKey?: boolean; outsidePress?: boolean }` | `true` | Listen in the capture phase, so a handler that stops the event still dismisses. `false` waits for the event to bubble to the document, so `stopPropagation()` in a handler keeps the layer open. | "Outside" means the event target is not inside the layer element, not inside any element `inside()` returns, and not inside a layer stacked above: a nested popover rendered elsewhere still counts as inside its parent while it is open. Mouse and pen dismiss on pointerdown; touch dismisses on the click that follows, so a scroll gesture that starts outside does not close the layer. An outside press stops below a modal layer (`blockOutside`, or a layer inside a modal ``), at a layer whose callback prevented the dismiss, and at a layer the press is inside: a press inside a parent popover closes only the child. Focus moving outside closes every `focusOutside` layer it left, top first. An Escape that belongs to an IME composition never dismisses. A press on an element injected at the top of the page after the layer opened (a browser extension's popup) is not outside, and neither is a press on the scrollbar of the element pressed. `onScrollbar(e)` is that last test, exported for a template's own pointer handling. ## The layer | Member | What it does | |---|---| | `el`, `options` | What the layer was pushed with. | | `isTop()` | True while this is the topmost layer. | | `release()` | Removes the layer from the stack. Safe to call twice. | ## In a template of your own ```ehtml app/shared/templates/popup/index.ehtml import { pushLayer, type Layer } from "@elements/ui/behaviors"; interface PopupState { trigger?: HTMLElement; layer?: Layer; } ``` The trigger is `inside`, so clicking it toggles the popup instead of dismissing and reopening it. Pair the layer with [float](floating) to place the popup and with [focusScope](focus-scope) when it takes focus. ## Writes `data-layer=""` on the element. With `blockOutside`, `pointer-events: none` on `` and `auto` on every open layer, put back when the last blocking layer releases. ## Used by [Chart](../templates/chart), [Combobox](../templates/combobox), [Dialog](../templates/dialog), [DropdownMenu](../templates/dropdown-menu), [ContextMenu](../templates/context-menu), [Menubar](../templates/menubar), [NavigationMenu](../templates/navigation-menu), [Popover](../templates/popover), [Select](../templates/select). `onScrollbar`: [Kanban](../templates/kanban), [RichText](../templates/rich-text), [TagsInput](../templates/tags-input).