create-composable
Workflow for creating SSR-safe, memory-safe, version-adaptive composables wrapping anime.js utilities.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Workflow for creating SSR-safe, memory-safe, version-adaptive composables wrapping anime.js utilities.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
Create complete documentation sites for projects. Use when asked to: "create docs", "add documentation", "setup docs site", "generate docs", "document my project", "write docs", "initialize documentation", "add a docs folder", "create a docs website". Generates Docus-based sites with search, dark mode, MCP server, and llms.txt integration.
Guide for creating pages in the playground for the module
Scaffolds a documentation sample for a new composable using the project's standard structure.
Guide for creating effective skills. This skill should be used when users want to create a new skill (or update an existing skill) that extends Claude's capabilities with specialized knowledge, workflows, or tool integrations.
Guide for creating utility tests in this project using Nuxt test utils and component mounting.
| name | create-composable |
| description | Workflow for creating SSR-safe, memory-safe, version-adaptive composables wrapping anime.js utilities. |
Every composable in src/runtime/app/composables/ wraps an anime.js utility with Vue 3 reactivity. Each composable must be:
4.3.5)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.
If the API was added after 4.3.5, the composable must handle its absence at runtime:
// Dynamic import with availability check
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:
/**
* @since animejs 4.4.0
* @remarks Returns a no-op instance when scrambleText is unavailable (animejs < 4.4.0)
*/
export function useScrambleText(...) {
// ...
}
For APIs available since 4.3.5 or earlier, use static imports — no version check needed.
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.
Always import runtime values from submodule paths, never the top-level 'animejs' barrel.
// Correct — submodule paths
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'
// Wrong — top-level barrel
import { animate, utils, waapi } from 'animejs'
Type-only imports from 'animejs' are fine — they don't affect bundling:
import type { AnimationParams, TargetsParam } from 'animejs'
Anime.js operates on DOM elements. On the server there is no DOM. Every composable must guard all DOM access.
import { useMounted } from '@vueuse/core'
const mounted = useMounted() // false on server, true after client mount
Rules:
mounted.value.document, window, Element, or any browser global outside a mounted guard.watchEffect / watch callbacks, early-return when !mounted.value.nextTick() which only executes client-side.shallowRef with a safe default (empty object, null, or a no-op anime instance that doesn't touch DOM).// Pattern A: null init (preferred for version-adaptive composables)
const instance = shallowRef<SomeAnimeType | null>(null)
// Pattern B: no-op instance (for composables returning toReactive — needs a non-null seed)
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.
Every anime instance holds references to DOM nodes, tween state, and requestAnimationFrame handles. Leaking these causes:
import { tryOnScopeDispose } from '@vueuse/core'
// Always revert the anime instance when the composable's scope ends
tryOnScopeDispose(() => {
instance.value?.revert()
instance.value = null // release reference
})
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)
// Clean up previous instance before creating new one
if (instance.value) instance.value.revert()
instance.value = someAnimeFactory(targets, toValue(parameters) || {})
})
tryOnScopeDispose calls .revert() on the instance.revert()-ed before replacement in watch callbacksshallowRef arrays (like useSplitText's lines/words/chars) are reset to [] on disposesetInterval / setTimeout / requestAnimationFrame left without cleanupSome 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) {
// Inside a component: use watchEffect for reactive re-creation
watchEffect(() => { ... })
tryOnScopeDispose(() => { ... })
}
else {
// Outside a component (e.g., in a utility): one-shot via nextTick
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).
| 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 |
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.
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.
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.
src/module.ts:37-44src/runtime/app/utils/types.ts. This file is aliased as #nanime/types — users and example components import types from there, not directly from 'animejs'.
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/content/2.composables/ using the scaffold-composable-sample skill. This is required, not optional.docs/app/components/content/examples/composables/. Referenced by the docs page via ::render-code-block-preview. This is required.docs/content/2.composables/0.introduction.md linking to the new docs page.playground/pages/ (use the create-playground-page skill)test/suites/utilities/ (use the create-utility-tests skill)pnpm test:types # Must pass — no any, no unsafe casts
pnpm test # All 4 vitest projects must pass
pnpm dev # Verify in playground — SSR + client navigation
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.
useMounted() or nextTick()document, window, Element) at module/composable top levelshallowRef (null or no-op instance)tryOnScopeDispose reverts and nullifies the instance@since tagany, no as castsnormalize-targets helpersuse<Name>.ts exporting function use<Name>NANIME_INSTANCE (symbol trap or markNanimeInstance)optimizeDeps if neededsrc/runtime/app/composables/docs/content/2.composables/docs/app/components/content/examples/composables/docs/content/2.composables/0.introduction.mdplayground/pages/test/suites/utilities/pnpm test:types and pnpm test pass