Presence
The presence machine is a state machine that helps you manage exit animations in your project.
In the DOM, when a component is removed from the tree or hidden, it's removed instantly. The presence machine suspends the component to allow its exit animation before removing or hiding it.
The presence machine requires using CSS animations to animate the component's exit.
Installation
To use the presence machine in your project, run the following command in your command line:
npm install @zag-js/presence @zag-js/react # or yarn add @zag-js/presence @zag-js/react
npm install @zag-js/presence @zag-js/vue # or yarn add @zag-js/presence @zag-js/vue
npm install @zag-js/presence @zag-js/vue # or yarn add @zag-js/presence @zag-js/vue
npm install @zag-js/presence @zag-js/solid # or yarn add @zag-js/presence @zag-js/solid
This command will install the framework agnostic presence logic and the reactive utilities for your framework of choice.
Usage
First, import the presence package into your project
import * as presence from "@zag-js/presence"
The presence package exports two key functions:
machine
— The state machine logic for the presence.connect
— The function that translates the machine's state to JSX attributes and event handlers.
Next, import the required hooks and functions for your framework and use the presence machine in your project 🔥
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 [state, send] = useMachine(presence.machine({ present }), { context: { present, onExitComplete }, }) const api = presence.connect(state, send, 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/vue" import { computed, defineComponent } from "vue" interface PresenceProps { present: boolean unmountOnExit?: boolean } export const Presence = defineComponent<PresenceProps>({ name: "Presence", props: ["present", "unmountOnExit"], emits: ["exit-complete"], setup(props, { emit }) { const nodeRef = ref<HTMLElement | null>(null) const [state, send] = useMachine(presence.machine({ present }), { context: { present, onExitComplete: () => emit("exit-complete") }, }) const apiRef = computed(() => presence.connect(state.value, send, normalizeProps), ) watch(nodeRef, () => { apiRef.value.setNode(nodeRef.value) }) return () => { const api = apiRef.value const unmount = !api.present && props.unmountOnExit if (unmount) return null return ( <div hidden={!api.present} data-state={api.skip ? undefined : props.present ? "open" : "closed"} ref={nodeRef} {...props} /> ) } }, })
<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 {present, unmountOnExit } = defineProps<{ present: boolean unmountOnExit?: boolean }>() const emit = defineEmits<{ (e: "exit-complete"): void }>() const [state, send] = useMachine(presence.machine({ present }), { context: { present, onExitComplete: () => emit("exit-complete") }, }) const api = computed(() => presence.connect(state.value, send, 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>
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 [localProps, restProps] = splitProps(props, [ "present", "unmountOnExit", "onExitComplete", ]) const [state, send] = useMachine(presence.machine({ present }), { context: { present, onExitComplete }, }) const api = createMemo(() => presence.connect(state, send, 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} {...restProps} /> </Show> ) }
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:
present
boolean
Whether the node is present (controlled by the user)onExitComplete
() => void
Function called when the animation ends in the closed stateimmediate
boolean
Whether to synchronize the present change immediately or defer it to the next frame
Machine API
The presence api
exposes the following methods:
skip
boolean
Whether the animation should be skipped.present
boolean
Whether the node is present in the DOM.setNode
(node: HTMLElement) => void
Function to set the node (as early as possible)unmount
() => void
Function to programmatically unmount the node
Edit this page on GitHub