Manual UI Behaviors presence

presence

elements man ui/behaviors/presence

Keeps a part mounted while its exit animation plays. The runtime removes a node the moment its e:if turns false, so a part that animates out has to stay rendered after open goes false. A Presence is the small reactive object that does that, and presence() keeps it in step with open.

Signatures
function createPresence(read: () => boolean): Presence; function presence(p: Presence, read: () => boolean, opts?: PresenceOptions): () => void; function animationsDone(el: Element | null): Promise<void>;

createPresence reads open once, untracked, so a private initializer does not re-run. presence returns stop; store it on p.stop and call it from onremove. animationsDone resolves when every finite animation and transition on el and its subtree has finished, for a controller that waits on an animation of its own.

The Presence

Field Type What it is
mounted boolean Render the part while true: e:if={p.mounted}.
state "open" | "closed" Render as data-state={p.state}; CSS runs the enter animation off [data-state=open] and the exit off [data-state=closed].
phase "entering" | "open" | "leaving" | "closed" The whole lifecycle. Anything a leaving part must stop doing (being a Tab stop, being measured) reads the phase, not the open flag: a leaving part is still on screen.
stop () => void Set by presence(); call it from onremove.

When open turns true: mounted is true, state is "open", phase is "entering" until the enter animation ends. When it turns false: state is "closed" and phase "leaving"; once every finite animation and transition on the element has finished, phase is "closed" and mounted is false. With no animation (reduced motion, a theme with zero durations) it unmounts on the next frame. Reopening during the exit cancels the unmount.

Options

Option Type Default What it does
element () => Element | null none The animated element, the one carrying data-state. Without it the part unmounts on the next frame, skipping any exit animation.
onEnterEnd () => void none Called once the enter animations finish after the part opens. Not called for a part that starts open.
onExitStart () => void none Called when the exit starts, as state turns "closed".
onExitEnd () => void none Called after the exit animations finish, just before mounted turns false.
inertWhileLeaving boolean false Makes the element inert from the moment it starts to leave, so Tab and the pointer pass it by while it animates out.

With element, the part also gets two transition attributes: data-starting-style for its first frame after it opens (style the hidden look there and a CSS transition runs to the open one) and data-ending-style while it exits (style the hidden look there too, and the unmount waits for that transition).

In a template of your own

app/shared/templates/panel/index.ehtml
import { createPresence, presence, type Presence } from "@elements/ui/behaviors"; <Panel (open: boolean = false, private p: Presence = createPresence(() => open))> <div class="panel" oninsert={(e) => p.stop = presence(p, () => open, { element: () => (e.target as HTMLElement).firstElementChild })} onremove={() => p.stop?.()}> <div e:if={p.mounted} class="panel-content" data-state={p.state}><slot/></div> </div> </Panel>
app/shared/templates/panel/style.css
.panel-content[data-state="open"] { animation: panel-in var(--duration-normal) var(--ease-out); } .panel-content[data-state="closed"] { animation: panel-out var(--duration-normal) var(--ease-out); } @keyframes panel-in { from { opacity: 0; transform: translateY(-4px); } } @keyframes panel-out { to { opacity: 0; transform: translateY(-4px); } }

Writes

p.mounted, p.state and p.phase; on element(), data-starting-style for the first frame, data-ending-style during the exit, and inert while leaving with inertWhileLeaving.

Used by

Combobox (opens in a new tab), Dialog (opens in a new tab), DropdownMenu (opens in a new tab), ContextMenu (opens in a new tab), Menubar (opens in a new tab), NavigationMenu (opens in a new tab), Popover (opens in a new tab), Select (opens in a new tab). animationsDone alone: Collapsible (opens in a new tab), Sheet (opens in a new tab), Toast (opens in a new tab).