vanimate-presence
v0.2.0
Published
Simple AnimatePresence-like DOM persistence for VanJS
Readme
VanimatePresence
VanimatePresence is a DOM-first transition engine for plain DOM and VanJS.
Phase 1 covers:
- exit persistence for direct nodes
- optional enter transitions
- same-slot view switching
- built-in
crossfade() - custom enter/exit switch transitions
popLayoutsibling reflow
What It Solves
Use VanimatePresence(node, options) when you want a single node to enter or exit without disappearing immediately.
Use VanimatePresence.switch(...) when you want one view to replace another in the same slot without manually stacking, sizing, or cleaning up overlapping layers.
Install
npm install vanimate-presenceDirect Node API
import {
VanimatePresence,
webEnter,
webExit,
cssEnter,
cssExit,
} from "vanimate-presence";
const card = VanimatePresence(document.createElement("div"), {
enter: webEnter([{ opacity: 0 }, { opacity: 1 }], {
duration: 180,
fill: "both",
}),
exit: webExit([{ opacity: 1 }, { opacity: 0 }], {
duration: 180,
fill: "forwards",
}),
mode: "sync",
});VanimatePresence(node, options)
Registers an HTMLElement so the library can manage its lifecycle transitions.
options supports:
enterOptional. Runs when the managed element is inserted into the DOM.exitRequired. Runs when the managed element is removed.mode"sync"or"popLayout".layoutAdditional layout options forpopLayout.
Transition Helpers
enter and exit can each be one of:
- a callback:
(element) => unknown webEnter(...)orwebExit(...)cssEnter(...)orcssExit(...)
Web Animations example:
const node = VanimatePresence(document.createElement("div"), {
enter: webEnter(
[{ opacity: 0, transform: "translateY(8px)" }, { opacity: 1, transform: "translateY(0)" }],
{ duration: 180, easing: "ease-out", fill: "both" },
),
exit: webExit(
[{ opacity: 1, transform: "translateY(0)" }, { opacity: 0, transform: "translateY(-8px)" }],
{ duration: 180, easing: "ease-in", fill: "forwards" },
),
});CSS example:
const node = VanimatePresence(document.createElement("div"), {
enter: cssEnter("card-enter", {
waitFor: "animationend",
timeoutMs: 240,
}),
exit: cssExit("card-exit", {
waitFor: "animationend",
timeoutMs: 240,
}),
});Callback example:
const node = VanimatePresence(document.createElement("div"), {
enter: (element) => {
element.style.opacity = "1";
},
exit: (element) => element.animate(
[{ opacity: 1 }, { opacity: 0 }],
{ duration: 150, fill: "forwards" },
).finished,
});mode
syncThe exiting node remains in normal layout flow until its exit finishes.popLayoutThe exiting node is popped out of flow immediately and surviving siblings are animated into their new positions.
layout
layout is only used with mode: "popLayout".
Available options:
animateSiblings?: booleandurationMs?: numbereasing?: string
Example:
const node = VanimatePresence(document.createElement("div"), {
mode: "popLayout",
layout: {
animateSiblings: true,
durationMs: 320,
easing: "cubic-bezier(0.22, 1, 0.36, 1)",
},
exit: cssExit("fade-out", {
waitFor: "animationend",
timeoutMs: 320,
}),
});Prototype Attachment
The library installs a prototype helper automatically:
document.querySelector("#target")?.VanimatePresence({
exit: cssExit("fade-out", {
waitFor: "animationend",
timeoutMs: 240,
}),
});Use this when element creation happens somewhere else and you only want to attach lifecycle management.
Switch API
VanimatePresence.switch(...) creates a controller-backed host for same-slot view transitions.
It returns:
elementThe host element you mount into the DOM.setActive(key)Switches to the next view.destroy()Clears the host and cancels any in-flight switch transition.
Basic switch()
const switcher = VanimatePresence.switch({
initial: "upload",
views: {
upload: () => UploadView(),
gallery: () => GalleryView(),
},
transition: VanimatePresence.crossfade({ durationMs: 220 }),
});
document.body.append(switcher.element);
switcher.setActive("gallery");The switch host:
- mounts the initial view immediately
- overlaps outgoing and incoming views during a switch
- assigns stacking automatically
- locks host size during overlap
- removes stale layers after cleanup
VanJS Usage
VanJS state remains the source of truth. The switch controller only handles DOM orchestration.
import van from "vanjs-core";
import { VanimatePresence } from "vanimate-presence";
const view = van.state<"upload" | "gallery">("upload");
const switcher = VanimatePresence.switch({
initial: view.val,
views: {
upload: () => UploadView(),
gallery: () => GalleryView(),
},
transition: VanimatePresence.crossfade({ durationMs: 220 }),
});
van.derive(() => {
switcher.setActive(view.val);
});VanimatePresence.transition(...)
Use transition(...) to provide an explicit custom enter/exit pair.
const switcher = VanimatePresence.switch({
initial: "upload",
views: {
upload: () => UploadView(),
gallery: () => GalleryView(),
},
transition: VanimatePresence.transition({
enter: webEnter(
[
{ opacity: 0, filter: "blur(8px)", transform: "translateY(-16px)" },
{ opacity: 1, filter: "blur(0px)", transform: "translateY(0)" },
],
{ duration: 240, easing: "ease", fill: "both" },
),
exit: webExit(
[
{ opacity: 1, filter: "blur(0px)", transform: "translateY(0)" },
{ opacity: 0, filter: "blur(8px)", transform: "translateY(16px)" },
],
{ duration: 240, easing: "ease", fill: "forwards" },
),
stack: "incoming-above",
}),
});VanimatePresence.crossfade(...)
crossfade() is the built-in switch preset.
const transition = VanimatePresence.crossfade({
durationMs: 220,
easing: "ease",
});It expands to:
- enter: opacity
0 -> 1 - exit: opacity
1 -> 0 - stack:
"incoming-above"
Interruption Rules
Switch transitions use replace behavior in Phase 1.
If the active key changes again before cleanup completes:
- the newest key wins
- stale exiting layers are removed
- the interrupted incoming layer is reused as the next outgoing layer when needed
API Summary
Named exports:
VanimatePresencewebEnterwebExitcssEntercssExitinstallVanimatePresencePrototype
Static helpers on VanimatePresence:
VanimatePresence.switch(...)VanimatePresence.transition(...)VanimatePresence.crossfade(...)
Build
npm run buildTests
npm run testLint And Format
npm run lint
npm run format
npm run checkDemo
npm run demoThen open:
http://127.0.0.1:8000/demo/index.html