| name | create-composable |
| description | Workflow for creating SSR-safe, memory-safe, version-adaptive composables wrapping anime.js utilities. |
Overview
Every composable in src/runtime/app/composables/ wraps an anime.js utility with Vue 3 reactivity. Each composable must be:
- SSR safe โ never touches DOM or browser APIs during server render
- Memory safe โ cleans up all anime instances, watchers, and DOM mutations
- Version adaptive โ gracefully handles anime.js APIs that may not exist in older supported versions (minimum:
4.3.5)
Step 0: Check Version Availability
Before writing any code, determine whether the anime.js API you're wrapping exists across the supported version range.
Supported range: animejs >=4.3.5 (from peerDependencies in package.json)
Reference โ API availability by version:
| API | Module path | Available since |
|---|
animate() | animejs/animation | 4.0.0 |
createAnimatable() | animejs/animatable | 4.0.0 |
createDraggable() | animejs/draggable | 4.0.0 |
createTimeline() | animejs/timeline | 4.0.0 |
splitText() | animejs/text | 4.0.0 |
waapi.animate() | animejs/waapi | 4.0.0 |
| scrambleText() | animejs/text (scramble) | 4.4.0 |
| globals, forEachChildren, addChild, removeChild | animejs | 4.4.0 |
To verify against the submodule directly:
cd anime-core/anime
git log --oneline --all -- src/<module>/<feature>.js
git tag --contains <first-commit-hash> | sort -V | head -1
Or check the anime.js GitHub releases for the changelog.
Version-adaptive pattern
If the API was added after 4.3.5, the composable must handle its absence at runtime:
async function resolveAnimeModule<T>(modulePath: string): Promise<T | null> {
try {
return await import(modulePath)
}
catch {
return null
}
}
Use this for any API added in 4.3.6+. The composable should:
- Return a no-op stub or warn once when the API is unavailable
- Never throw โ the app should still work, just without that feature
- Document the minimum version in the composable's JSDoc and its docs page
export function useScrambleText(...) {
}
For APIs available since 4.3.5 or earlier, use static imports โ no version check needed.
Step 1: Scaffold the File
Create src/runtime/app/composables/use<Name>.ts.
No registration needed โ addImportsDir in src/module.ts:60-62 auto-imports everything in the directory.
If the composable wraps a new anime submodule path (e.g., animejs/text for scramble), add it to the Vite optimizeDeps list in src/module.ts:37-44.
Import rules
Always import runtime values from submodule paths, never the top-level 'animejs' barrel.
import { animate } from 'animejs/animation'
import { createAnimatable } from 'animejs/animatable'
import { createDraggable } from 'animejs/draggable'
import { createTimeline } from 'animejs/timeline'
import { splitText } from 'animejs/text'
import { waapi } from 'animejs/waapi'
import { set, stagger, round } from 'animejs/utils'
import { animate, utils, waapi } from 'animejs'
Type-only imports from 'animejs' are fine โ they don't affect bundling:
import type { AnimationParams, TargetsParam } from 'animejs'
Step 2: SSR Safety
Anime.js operates on DOM elements. On the server there is no DOM. Every composable must guard all DOM access.
Required guards
import { useMounted } from '@vueuse/core'
const mounted = useMounted()
Rules:
- Never call anime functions at the top level of the composable. Always gate behind
mounted.value.
- Never access
document, window, Element, or any browser global outside a mounted guard.
- In
watchEffect / watch callbacks, early-return when !mounted.value.
- In static (non-watchable) mode, use
nextTick() which only executes client-side.
- Initialize
shallowRef with a safe default (empty object, null, or a no-op anime instance that doesn't touch DOM).
SSR-safe initialization patterns
const instance = shallowRef<SomeAnimeType | null>(null)
const instance = shallowRef(animate({}, {}))
Pattern B is used by useAnimate, useAnimatable, useWaapiAnimate โ the empty animate({}, {}) creates a lightweight no-op. Use this when the composable returns toReactive(instance) since toReactive(null) would break.
Pattern A is used by useDraggable, useSplitText โ they return custom objects, so null is safe as the initial value.
Step 3: Memory Safety
Every anime instance holds references to DOM nodes, tween state, and requestAnimationFrame handles. Leaking these causes:
- DOM nodes retained after component unmount
- Orphaned animation loops ticking in the background
- Memory growth on repeated navigation (SPA route changes)
Required cleanup
import { tryOnScopeDispose } from '@vueuse/core'
tryOnScopeDispose(() => {
instance.value?.revert()
instance.value = null
})
Cleanup on re-creation
When a watchable composable re-creates the anime instance (target changed, params changed), revert the old one first:
watchEffect(() => {
if (!mounted.value) return
const targets = normalizeAnimeTarget(target)
if (instance.value) instance.value.revert()
instance.value = someAnimeFactory(targets, toValue(parameters) || {})
})
Cleanup checklist
Step 4: Reactivity Pattern
Dual-mode composables (Watchable vs Instant)
Some composables support two modes based on whether they're called inside a Vue component instance:
import { AnimationComponentFlags, getAnimationComponentFlag } from '../utils/normalizers/instance-management'
const flag = getAnimationComponentFlag()
if (flag === AnimationComponentFlags.Watchable) {
watchEffect(() => { ... })
tryOnScopeDispose(() => { ... })
}
else {
nextTick(() => { ... })
}
Used by: useAnimate, useAnimatable, useAnimeTimeline, useScrambleText, useWaapiAnimate
Not used by: useDraggable, useSplitText (always watchable)
Use dual-mode when the composable wraps a simple animate-like call. Skip it when the composable manages complex state (drag controllers, text splitters).
Return types
| When returning | Pattern | Used by |
|---|
| The anime instance directly | return toReactive(shallowRef) | useAnimate, useAnimatable, useScrambleText, useWaapiAnimate |
| A proxy with methods | return createProxy(shallowRef) | useDraggable |
| A buffered proxy with chaining | return createBufferedProxy(shallowRef, opts) | useAnimeTimeline |
| A custom object with refs | Return { ref1, ref2, computed1 } | useSplitText |
Target normalization
Always normalize targets through the helpers in ../utils/normalize-targets:
-
normalizeAnimeTarget โ for standard anime targets (string, ref, element)
-
normalizeWaapiAnimeTarget โ for WAAPI targets
-
normalizeSplitTextTarget โ for text splitting targets
-
normalizeDraggableContainer โ for draggable containers
Add a new normalizer if the API expects a different target shape.
NANIME_INSTANCE identification
Every composable return value must be identifiable as a nanime proxy so that useAnimeTimeline's .sync() can unwrap it to the raw anime.js instance.
createProxy / createBufferedProxy returns: Already have NANIME_INSTANCE symbol in their get/has traps โ no extra work needed.
toReactive returns: Must be registered via markNanimeInstance(result, instanceRef) from ../utils/create-proxy before returning:
import { markNanimeInstance } from '../utils/create-proxy'
const result = toReactive(instance)
markNanimeInstance(result, instance)
return result
This enables resolveNanimeInstance() to extract the raw anime.js instance from any nanime composable return, regardless of proxy strategy.
Reffable props
For composables where individual options should be reactive (like useDraggable), use the makeReffable pattern from ../utils/normalizers/make-reffable.ts:
import { normalizeReffable, type MakeRefable } from '../utils/normalizers/make-reffable'
type Options = MakeRefable<OriginalParams, 'prop1' | 'prop2', InstanceType>
This lets users pass either a raw value or a Ref/getter for those props.
Step 5: Integrate
- Vite optimizeDeps โ If wrapping a new anime submodule, add to
src/module.ts:37-44
- Types โ Re-export any new anime.js types used by the composable from
src/runtime/app/utils/types.ts. This file is aliased as #nanime/types โ users and example components import types from there, not directly from 'animejs'.
- If the composable exposes anime.js utilities that consuming apps need (e.g.,
scrambleText for use inside animation params), create a re-export under src/runtime/app/utils/proxies/ and register it as a #nanime/proxies/<name> alias in src/module.ts. This avoids direct animejs/* imports in consuming apps.
- Docs page โ Create in
docs/content/2.composables/ using the scaffold-composable-sample skill. This is required, not optional.
- Example component โ Create a live demo in
docs/app/components/content/examples/composables/. Referenced by the docs page via ::render-code-block-preview. This is required.
- Composables index card โ Add a card entry in
docs/content/2.composables/0.introduction.md linking to the new docs page.
- Playground โ Create a test page in
playground/pages/ (use the create-playground-page skill)
- Tests โ Write utility tests in
test/suites/utilities/ (use the create-utility-tests skill)
Step 6: Validate
pnpm test:types
pnpm test
pnpm dev
Test SSR explicitly: load the playground page via full page refresh (server render), then navigate to it via client-side link (client render). Both must work without errors.
Checklist
SSR
Memory
Version Adaptivity
Code Quality
Deliverables