Guides
Server rendering (SSR)
What runs on the server, what does not, and how the initial style reaches the first paint.
Ani can be imported and rendered on the server without a DOM. Nothing animates there, and nothing leaks.
What is safe on the server
- Importing any package. No module touches
windowordocumentat import time. - Creating animation trees, timelines,
createStatesmachines and clocks. - Calling
play()andtransitionTo(). The shared clock finds no frame source (requestAnimationFrame) andsubscribebecomes a silent no-op: it does not throw and does not retain the timeline. The timeline holds its starting value but never advances. - Rendering with the bindings.
useAni,useAniRef,useAniGroup,useAniStatesanduseFollow(and the Solid, Vue and Svelte equivalents) render with their initial values. They subscribe to timelines only after mount, so a server-rendered tree retains no subscriptions. - Reduced motion. Without
matchMediathe preference is off on the server. See Reduced motion.
What needs the browser
waapiTimeline(playneedsKeyframeEffectanddocument.timeline). Call it in a client-only effect.bind(element)on a timeline, group or follower. The bindings call it on mount.
Initial styles and hydration
The element bindings (useAniRef, useAniGroup, useAniStates, useFollow and their equivalents) return a style built from the initial value (initialValue, or initial for useFollow). Render it on the element, and the server HTML already contains the starting style, so the first paint shows the initial value instead of an unstyled element.
import * as a from "@freestylejs/ani-core";
import { useAniRef, useTimeline } from "@freestylejs/ani-react";
export function Card() {
const timeline = useTimeline(() =>
a.rafTimeline(
a.ani({ to: { opacity: 1, translateY: 0 }, duration: 0.4 })
)
);
const { ref, style } = useAniRef(timeline, {
initialValue: { opacity: 0, translateY: 20 },
});
// `style` is rendered on the server: opacity 0 and translateY(20px)
return <div ref={ref} style={style} />;
}After mount, ref binds the element to the timeline, and the core writes every frame. The client renders the same style, so hydration does not mismatch, and re-rendering never resets an animated style.
- Vue:
:style="style". Solid:style={style()}. Svelte:{style}, a CSS declaration string. - Without
initialValue,styleisundefinedand the server renders no animated style. - Without a binding, build the same object with
createStyleSheet(values, resolver?). See Styles & Building Bindings.
Rules for your own code
- Do not call
play()during render. Start animations in an effect,onMountedoronMount. - Do not rely on animation callbacks (
onUpdate) firing on the server. - Test your components in a
nodeenvironment to catch accidental DOM access. See Testing.