Animations
Zoijs has no animation package, and doesn't need one: the platform already animates. CSS transitions handle state changes, and the Web Animations API (element.animate(...)) handles imperative ones — and the ref binding hands you the element to drive either. This recipe shows the two patterns that cover almost everything.
Why no@zoijs/animations? The easy part (animate on enter) is one line of platform code; the hard part (animating list removals, FLIP-style) needs a removal hook ineachthat the core doesn't expose — a future core decision, not a bolt-on package. See Decision 0007.
Enter: animate on insert with ref#
The ref callback runs right after the element is inserted (and connected), so it's the natural place to play an entrance animation:
import { html, mount } from "@zoijs/core";
const fadeIn = (el) =>
el.animate(
[{ opacity: 0, transform: "translateY(8px)" }, { opacity: 1, transform: "none" }],
{ duration: 200, easing: "ease-out" }
);
function Card() {
return html`<div class="card" ref=${fadeIn}>Hello</div>`;
}
mount(Card, "#app");That's the whole "enter" story — no wrapper, no config. It works inside a keyed each too: each new item's ref fires as it's inserted.
Toggle: animate with CSS#
For show/hide and state changes, plain CSS transitions are the simplest, most performant option. Bind a class and let the browser interpolate:
import { html, mount, createState } from "@zoijs/core";
function Panel() {
const open = createState(false);
return html`
<button onclick=${() => open.set(!open.get())}>Toggle</button>
<div class=${() => `panel ${open.get() ? "open" : ""}`}>Content</div>
`;
}.panel { max-height: 0; overflow: hidden; transition: max-height 0.25s ease; }
.panel.open { max-height: 200px; }Leave: animate, then remove#
To animate something out, keep the element in the DOM until its exit animation finishes, then drop it. Because the component owns the "is it present?" boolean, you animate first and flip the flag in the animation's finished promise — no framework hook needed:
import { html, mount, createState } from "@zoijs/core";
function Toast({ message }) {
const present = createState(true);
const dismiss = (e) => {
const el = e.target.closest(".toast");
el.animate([{ opacity: 1 }, { opacity: 0, transform: "translateX(20px)" }], {
duration: 180,
easing: "ease-in",
}).finished.then(() => present.set(false)); // remove after it plays
};
return html`
${() =>
present.get()
? html`<div class="toast">${message} <button onclick=${dismiss}>✕</button></div>`
: null}
`;
}The one hard case: list reordering#
When a keyed each reorders items, it moves the existing DOM nodes (the minimal set of moves) instead of recreating them — so the nodes survive, and a CSS transition: transform on the items will animate layout shifts in many browsers for free. A full FLIP (measure-before, animate-from) across a reorder is the one thing that would benefit from core support; until then, lean on transition: transform and per-item enter animations, which cover the common cases.
Next: Icons.