Presence
Presence helps manage mount/unmount transitions with exit animations.
When a component is hidden or removed, the DOM usually removes it immediately. Presence keeps it mounted long enough for exit animations to finish.
The presence machine requires using CSS animations to animate the component's exit.
Installation
Install the presence package:
npm install @zag-js/presence @zag-js/react # or yarn add @zag-js/presence @zag-js/react
npm install @zag-js/presence @zag-js/solid # or yarn add @zag-js/presence @zag-js/solid
npm install @zag-js/presence @zag-js/vue # or yarn add @zag-js/presence @zag-js/vue
npm install @zag-js/presence @zag-js/svelte # or yarn add @zag-js/presence @zag-js/svelte
Usage
Import the presence package:
import * as presence from "@zag-js/presence"
The presence package exports two key functions:
machine- State machine logic.connect- Maps machine state to JSX props and event handlers.
Then use the framework integration helpers:
import * as presence from "@zag-js/presence" import { useMachine, normalizeProps } from "@zag-js/react" interface PresenceProps { present: boolean unmountOnExit?: boolean onExitComplete?: () => void } function Presence(props: PresenceProps) { const { unmountOnExit, present, onExitComplete, ...rest } = props const service = useMachine(presence.machine, { present, onExitComplete, }) const api = presence.connect(service, normalizeProps) if (!api.present && unmountOnExit) return null return ( <div hidden={!api.present} data-state={api.skip ? undefined : present ? "open" : "closed"} ref={api.setNode} {...rest} /> ) }
import * as presence from "@zag-js/presence" import { useMachine, normalizeProps } from "@zag-js/solid" import { createMemo, createUniqueId, JSX } from "solid-js" interface PresenceProps { present: boolean unmountOnExit?: boolean onExitComplete?: () => void children: JSX.Element } function Presence(props: PresenceProps) { const [machineProps, localProps] = splitProps(props, [ "present", "unmountOnExit", "onExitComplete", ]) const service = useMachine(presence.machine, machineProps) const api = createMemo(() => presence.connect(service, normalizeProps)) const unmount = createMemo(() => !api().present && localProps.unmountOnExit) return ( <Show when={!unmount()}> <div hidden={!api().present} data-state={api().skip ? undefined : present ? "open" : "closed"} ref={api().setNode} {...localProps} /> </Show> ) }
<script setup lang="ts"> import * as presence from "@zag-js/presence" import { useMachine, normalizeProps } from "@zag-js/vue" import { computed, watch, ref } from "vue" const props = defineProps<{ present: boolean unmountOnExit?: boolean }>() const emit = defineEmits<{ (e: "exit-complete"): void }>() const service = useMachine(presence.machine, { get present() { return props.present }, onExitComplete: () => emit("exit-complete"), }) const api = computed(() => presence.connect(service, normalizeProps)) const nodeRef = ref<HTMLElement | null>(null) watch(nodeRef, () => { api.value.setNode(nodeRef.value) }) const unmount = computed(() => !api.value.present && unmountOnExit) </script> <template> <div v-show="!unmount" :hidden="!api.present" :data-state="api.skip ? undefined : present ? 'open' : 'closed'" ref="nodeRef" v-bind="$attrs" /> </template>
<script lang="ts"> import * as presence from "@zag-js/presence" import { normalizeProps, useMachine } from "@zag-js/svelte" import type { Snippet } from "svelte" interface Props { present: boolean unmountOnExit?: boolean onExitComplete?: () => void children?: Snippet } let { present, unmountOnExit, onExitComplete, children }: Props = $props() const service = useMachine(presence.machine, () => ({ present, onExitComplete, })) const api = $derived(presence.connect(service, normalizeProps)) function setNode(node: HTMLDivElement) { api.setNode(node) } const unmount = $derived(!api.present && unmountOnExit) </script> {#if !unmount} <div {@attach setNode} hidden={!api.present} data-state={api.skip ? undefined : present ? "open" : "closed"} > {@render children?.()} </div> {/if}
Running code after exit animation
Use onExitComplete to run logic after the exit animation finishes.
const service = useMachine(presence.machine, { present: open, onExitComplete() { console.log("Exit animation finished") }, })
Applying presence changes immediately
Set immediate to true to skip deferring present-state changes to the next
frame.
const service = useMachine(presence.machine, { present: open, immediate: true, })
Styling guide
To style any entry and exit animations, set up the @keyframes and apply the
animations.
@keyframes enter { from { scale: 0.9; opacity: 0; } to { opacity: 1; scale: 1; } } @keyframes exit { to { opacity: 0; scale: 0.9; } } [data-state="open"] { animation: enter 0.15s ease-out; } [data-state="closed"] { animation: exit 0.1s ease-in; }
You can then use the Presence component in your project.
function Example() { const [open, setOpen] = React.useState(true) return ( <> <button onClick={() => setOpen((c) => !c)}>Toggle</button> <Presence present={open} unmountOnExit> <div>Content</div> </Presence> </> ) }
Methods and Properties
Machine Context
The presence machine exposes the following context properties:
presentbooleanWhether the node is present (controlled by the user)onExitCompleteVoidFunctionFunction called when the animation ends in the closed stateimmediatebooleanWhether to synchronize the present change immediately or defer it to the next frame
Machine API
The presence api exposes the following methods:
skipbooleanWhether the animation should be skipped.presentbooleanWhether the node is present in the DOM.setNode(node: HTMLElement) => voidFunction to set the node (as early as possible)unmountVoidFunctionFunction to programmatically unmount the node