Migrating from 1.x
Move a codebase from ANI 1.x to 2.0 in one cut. Run the codemod, fix what it reports, and check the behavior changes that type-checking cannot catch.
Why 2.0
ANI 2.0 computes every frame first and writes the DOM once, in a commit phase, so layout reads and writes never interleave. A rafGroup drives a whole list with one plan and one clock subscription. rafTimeline and waapiTimeline share one semantics: the same options, run results and validation. Reduced motion is honored by default, and every duration, delay and offset is in seconds, including the delay of play(). The price is a breaking API: 1.x names are removed rather than deprecated, and 1.x call shapes throw instead of being guessed. This guide moves a 1.x codebase over in one pass.
Run the codemod first
The codemod rewrites every call site that has a safe 2.0 form and reports the rest with file, line and column.
npx @freestylejs/ani-codemod srcPass every directory that contains ANI code. Directories are walked recursively (node_modules, dist, .git and .next are skipped). The codemod handles .ts, .mts, .cts, .tsx, .js, .mjs, .cjs, .jsx, .vue, .svelte, .md and .mdx files.
Commit before you run it, and run it once. The codemod cannot tell seconds that were already converted from milliseconds, so a second run divides every delay again.
Preview with --dry
--dry prints the warnings and the summary without writing any file.
npx @freestylejs/ani-codemod --dry srcEach warning has the form file:line:column [rule] message, followed by N files changed, M warnings. The exit code is 0, or 1 on a usage, IO or syntax error (other files are still processed).
Read every warning
Nothing is changed silently: whatever the codemod cannot decide is left as it was and reported. Each rule has a section below.
| Rule | Section |
|---|---|
ambiguous-play | Ambiguous play |
reset-arguments | Reset arguments |
property-resolver | Property resolver |
context-removed | Context removed |
clock-reset | Clock reset |
clock-create | Clock create |
clock-configure-argument | Clock configure argument |
react-use-ani-ref | React useAniRef |
Type-check next
Removed APIs do not type-check in 2.0. After the codemod, run tsc --noEmit and fix what is left with the table below. Untyped code gets a different safety net: 1.x call shapes throw a TypeError at the first call.
Every change at a glance
The Codemod column says what ani-codemod does:
- yes: the call site is rewritten.
- partly: only some call shapes are rewritten, or the code is changed and a warning asks for a manual follow-up.
- no: the code is not touched. A warning may still point at it.
| 1.x | 2.0 | Codemod |
|---|---|---|
import { a } from '@freestylejs/ani-core' | import * as a from '@freestylejs/ani-core' | yes |
T (T.linear(), T.spring(...)) | a.timing, or import { timing } | no |
a.dynamicTimeline(node) | a.rafTimeline(node) | yes |
a.timeline(node), a.webTimeline(node) | a.waapiTimeline(node) | yes |
a.rafTimeline(node, clock) | a.rafTimeline(node, { clock }) | yes |
a.sequence(children, timing, id), a.parallel(children, timing, id) | a.sequence(children, { timing, id }) | yes |
a.loop(node, count, timing, id) | a.loop(node, count, { timing, id }) | yes |
a.stagger(children, offset, timing, id) | a.stagger(children, { offset, timing, id }) | yes |
a.delay(seconds, id) | a.delay(seconds, { id }) | yes |
a.ani(props, id) | a.ani({ ...props, id }) | yes |
loop(node, n) with keyframes / durations entries per iteration | one entry per ani / delay node as written in the tree | no |
timeline.getCurrentValue(), timeline.getCurrentVelocity(), timeline.currentConfig | timeline.value, timeline.velocity, timeline.config | yes |
timeline.play(config, canBeIntercepted) | timeline.play({ ...config, interrupt }) | yes |
play(x, y) that matches no known 1.x shape | decide RAF or WAAPI by hand (Ambiguous play) | partly |
timeline.reset(notify, unsubscribeClock) | timeline.reset() | partly |
delay: 500 in a play or transitionTo config (milliseconds) | delay: 0.5 (seconds) | partly |
RAF play({ propertyResolver }) | timeline.bind(target, { resolver }) | no |
RAF play({ context }) | removed (Context removed) | no |
play() and transitionTo() return nothing | they return a Promise<RunResult> that never rejects | no |
onUpdate callbacks run inside the frame update | they run in the commit phase | no |
WAAPI play(element, { propertyResolver, keyframeEffect }) | play(element, { resolver, effect }) | yes |
WAAPI timeline.nativeAnimation | timeline.animations[0] | yes |
WAAPI play() returns Animation | null, one target per timeline | it returns a Promise<RunResult> and takes one element or a list | no |
createStates({ initialFrom }) | createStates({ initialValue }) | yes |
machine.onTimelineChange((timeline) => ...) | machine.onTransition((state, timeline) => ...) | yes |
machine.transitionTo(state, config, canBeIntercepted) | machine.transitionTo(state, { ...config, interrupt }) | yes |
AnimationClock.configure(0.1) | defaultClock.configure({ maxDeltaTime: 0.1 }) | yes |
AnimationClock.create() | defaultClock | yes |
AnimationClock.create(maxDeltaTime) | defaultClock.configure({ maxDeltaTime }), or createClock({ maxDeltaTime }) | no |
AnimationClock.reset() | resetDefaultClock() from @freestylejs/ani-core/testing, or dispose what you created | no |
clock.setTiming(options) | clock.configure(options) | no |
Error messages prefixed [AnimationClock] | prefixed [Clock] | no |
RafAniTimeline<G, Ctx>, WebAniTimeline<G> (types) | RafTimeline<G>, WaapiTimeline<G> | no |
RafTimelineConfig<G, Ctx>, WebAniTimelineConfig<G> | RafTimelineConfig<G>, WaapiPlayConfig<G> | no |
value instanceof RafAniTimeline, value instanceof WebAniTimeline | isRafTimeline(value), isWaapiTimeline(value) | no |
Node classes as values (SegmentNode, SequenceNode, ...) | removed; the types remain, build nodes with a.ani, a.sequence, ... | no |
dynamicSpring instance methods init() and reset() | removed; the spring state lives in the timeline | no |
EventManager, calculateSegmentState, PlanRuntime, TimelineBase | removed | no |
applyStyleSheet(element, style) | timeline.bind(element) (createStyleSheet stays for server rendering) | no |
Binding events and cleanupEvents options | your framework's event handlers | no |
Event handler context ctx.play(...), ctx.current | timeline.play(...), timeline.value | no |
React useAniRef(ref, { timeline, ... }) returning a controller | useAniRef(timeline, options) returning { ref, style } | no |
React useAniStates(ref, props) returning [{ state, timeline }, transitionTo] | useAniStates(config) returning { ref, style, state, transitionTo, machine } | no |
Vue, Solid and Svelte useAniRef, createAniRef returning [ref, controller] | useAniRef(timeline, options) returning { ref, style } (Svelte: { action, style }) | no |
Vue, Solid and Svelte useAniStates, createAniStates returning [ref, { state, timeline }, transitionTo] | one config in, { ref, style, state, transitionTo, machine } out (Svelte: action) | no |
initialFrom in binding configs | initialValue | no |
useAni and createAni keep their [value, controller] tuple. The timeline you pass them is still never disposed by the binding.
Manual changes
Ambiguous play
ambiguous-play marks a play or transitionTo call whose arguments match no known 1.x shape, such as timeline.play(x, y). The codemod tells the engines apart by the arguments: the WAAPI form is play(element, { ... }), the RAF form is play(config) or play(config, true | false).
Decide which engine the receiver is. For RAF, the flag becomes the interrupt key of the config. For WAAPI, the element comes first.
import * as a from '@freestylejs/ani-core'
const node = a.ani({ to: { opacity: 1 }, duration: 0.3 })
const raf = a.rafTimeline(node)
const waapi = a.waapiTimeline(node)
const element = document.createElement('div')
const config = { from: { opacity: 0 } }
const interruptible = false
raf.play({ ...config, interrupt: interruptible })
waapi.play(element, config)Reset arguments
reset-arguments means reset was called with arguments and the codemod removed them. In 1.x, reset(false) skipped the notification and reset(true, false) kept the timeline on the clock. In 2.0, reset() takes no arguments and always notifies subscribers once, with status IDLE and an empty state.
Check that your onUpdate callbacks handle that notification. A callback that wrote the state to the DOM should skip the IDLE frame.
import * as a from '@freestylejs/ani-core'
const timeline = a.rafTimeline(a.ani({ to: { opacity: 1 }, duration: 0.3 }))
const element = document.createElement('div')
const stop = timeline.onUpdate(({ state, status }) => {
if (status === 'IDLE') return // reset() notifies with an empty state
element.style.opacity = String(state.opacity)
})
timeline.reset()
stop()To reset without notifying, unsubscribe first or dispose the timeline.
Property resolver
property-resolver marks a RAF play or transitionTo config with propertyResolver. The key is removed from play configs because play() no longer writes styles. The style mapping belongs to the binding: bind(target, { resolver }).
import * as a from '@freestylejs/ani-core'
const timeline = a.rafTimeline(a.ani({ to: { progress: 100 }, duration: 1 }))
const element = document.createElement('div')
timeline.bind(element, {
resolver: {
progress: (value) => ({ key: '--progress', value: `${value}%` }),
},
})
timeline.play({ from: { progress: 0 } })The framework bindings take the same resolver in their options: useAniRef(timeline, { resolver }). See Styles & Building Bindings.
The WAAPI option is not removed: propertyResolver is renamed resolver (the codemod does this).
Context removed
context-removed marks a RAF config with context. The value was never read by the engine and the key is gone. Remove it and pass the values your callbacks need through a closure, the timeline or the nodes.
Clock reset
clock-reset marks AnimationClock.reset(), which has no direct equivalent in application code. Dispose the timelines and clocks you created instead; the clock stops its loop when the last subscriber leaves.
In tests, use resetDefaultClock() from @freestylejs/ani-core/testing. It disposes the shared clock and restores its defaults while keeping the same instance. See Testing.
import { resetDefaultClock } from '@freestylejs/ani-core/testing'
import { afterEach } from 'vitest'
afterEach(() => {
resetDefaultClock()
})Clock create
clock-create marks AnimationClock.create(maxDeltaTime) with an argument. AnimationClock.create() without arguments is rewritten to defaultClock. With an argument, choose between changing the shared clock and making your own.
import { createClock, defaultClock } from '@freestylejs/ani-core'
// the shared clock, reconfigured
defaultClock.configure({ maxDeltaTime: 0.1 })
// an independent clock for a separate engine
export const ownClock = createClock({ maxDeltaTime: 0.1 })configure takes an options object. A bare number is a type error, and in untyped code it throws ANI_INVALID_OPTIONS.
Clock configure argument
clock-configure-argument marks AnimationClock.configure(...) called with an argument that is neither a numeric literal nor an object literal. In ANI 2, configure takes { lagThreshold, maxDeltaTime }. If the value is a number, pass { maxDeltaTime: value }.
React useAniRef
react-use-ani-ref marks useAniRef(ref, props). The hook no longer takes a ref object. It takes the timeline and returns a ref callback and an initial style. See React.
Delay held in variables
The codemod converts delay only in object literals written inline in the play or transitionTo call. A config held in a variable, built by a helper or read from settings keeps its milliseconds, and TypeScript cannot tell: both are number. A delay: 500 that is not converted waits 500 seconds.
Search for delay in every config you build outside the call and divide by hand.
// 1.x
const config = { from: { opacity: 0 }, delay: 500 }
// 2.0
const config = { from: { opacity: 0 }, delay: 0.5 }The same applies to the delay option of WAAPI play() and of transitionTo(). The delay node, duration and stagger offsets were already in seconds.
Classes and instanceof
The timeline classes are no longer exported. Tell the engines apart with the type guards.
import { isRafTimeline, isWaapiTimeline } from '@freestylejs/ani-core'
export function engineOf(value: unknown): 'raf' | 'waapi' | null {
if (isRafTimeline(value)) return 'raf'
if (isWaapiTimeline(value)) return 'waapi'
return null
}Replace the types too: RafAniTimeline becomes RafTimeline, WebAniTimeline becomes WaapiTimeline. isRafTimeline is also true for the controller facade that the framework bindings hand out.
Events
EventManager and the events option of the bindings are removed. They attached listeners for you and passed a context (ctx) that held the controller methods and ctx.current. In 2.0 the DOM is the framework's job: attach handlers the way the framework does, and call the timeline directly. ctx.current becomes timeline.value.
Without a framework, use addEventListener and remove the listener when the owner goes away.
import * as a from '@freestylejs/ani-core'
const timeline = a.rafTimeline(a.ani({ to: { translateX: 300 }, duration: 0.5 }))
const element = document.createElement('div')
const unbind = timeline.bind(element)
const onPointerDown = (event: PointerEvent) => {
void timeline.play({ from: { translateX: event.clientX } })
}
element.addEventListener('pointerdown', onPointerDown)
// when the owner goes away
element.removeEventListener('pointerdown', onPointerDown)
unbind()
timeline.dispose()Writing styles
applyStyleSheet(element, style) is removed. timeline.bind(element) writes the value in the commit phase, only for declarations that changed. createStyleSheet(values, resolver?) stays for the first server-rendered paint.
Framework bindings
All four bindings change in the same way:
useAniRef(createAniRefin Solid) takes the timeline and an options object, and returns{ ref, style }. Svelte returns{ action, style }.styleis the initial inline style ofinitialValue. Put it on the element so the server renders it.eventsandcleanupEventsare gone. Use the framework's event handlers.useAniStates(createAniStates) takes one config and returns an object.useTimeline(createTimeline) is new. It creates a timeline, group or follower and disposes it with the component.
React
React no longer takes a ref object: useAniRef returns the ref callback, and the component owns the timeline with useTimeline.
// 1.x
const ref = useRef<HTMLDivElement>(null)
const timeline = useMemo(() => a.dynamicTimeline(node), [])
const controller = useAniRef(ref, {
timeline,
initialValue: { translateX: 0 },
events: { onPointerdown: (ctx, event) => ctx.play({ from: { translateX: event.clientX } }) },
})
return <div ref={ref} />import * as a from '@freestylejs/ani-core'
import { useAniRef, useTimeline } from '@freestylejs/ani-react'
const node = a.ani({ to: { translateX: 300 }, duration: 0.5 })
export function Box() {
const timeline = useTimeline(() => a.rafTimeline(node))
const { ref, style } = useAniRef(timeline, {
initialValue: { translateX: 0 },
})
return (
<div
ref={ref}
style={style}
onPointerDown={(event) => {
void timeline.play({ from: { translateX: event.clientX } })
}}
/>
)
}useAniStates loses the ref argument and returns an object:
// 1.x
const ref = useRef<HTMLButtonElement>(null)
const [{ state }, transitionTo] = useAniStates(ref, { initial: 'idle', initialFrom: { opacity: 0.7 }, states })
return <button ref={ref} data-state={state} />import * as a from '@freestylejs/ani-core'
import { useAniStates } from '@freestylejs/ani-react'
const states = {
idle: a.ani({ to: { opacity: 0.7 }, duration: 0.5 }),
hover: a.ani({ to: { opacity: 1 }, duration: 0.3 }),
}
export function StateButton() {
const { ref, style, state, transitionTo } = useAniStates({
initial: 'idle',
initialValue: { opacity: 0.7 },
states,
})
return (
<button
ref={ref}
style={style}
data-state={state}
onMouseEnter={() => void transitionTo('hover')}
onMouseLeave={() => void transitionTo('idle')}
/>
)
}The timeline member is gone from the result. Read the active timeline with machine.timeline().
Vue
useAniRef returns { ref, style } instead of [elementRef, controller]. Bind both in the template: :ref="ref" :style="style".
// 1.x
const [elementRef, controller] = useAniRef({
timeline: myTimeline,
initialValue: { translateX: 0 },
events: { onPointerdown: (ctx, event) => ctx.play({ from: { translateX: event.clientX } }) },
})
// <div :ref="elementRef" /><script setup>
import * as a from '@freestylejs/ani-core'
import { useAniRef, useTimeline } from '@freestylejs/ani-vue'
const timeline = useTimeline(() =>
a.rafTimeline(a.ani({ to: { translateX: 300 }, duration: 0.5 }))
)
const { ref, style } = useAniRef(timeline, {
initialValue: { translateX: 0 },
})
</script>
<template>
<div
:ref="ref"
:style="style"
@pointerdown="timeline.play({ from: { translateX: $event.clientX } })"
/>
</template>useAniStates returns { ref, style, state, transitionTo, machine } instead of [elementRef, { state, timeline }, transitionTo], and state is a readonly ref.
Solid
createAniRef takes the timeline (or an accessor of one) and returns { ref, style }. style is an accessor, so call it: style={style()}.
// 1.x
const myTimeline = createMemo(() => a.dynamicTimeline(node))
const [ref, controller] = createAniRef({
timeline: myTimeline,
initialValue: { translateX: 0 },
events: { onPointerdown: (ctx, event) => ctx.play({ from: { translateX: event.clientX } }) },
})
return <div ref={ref} />import * as a from '@freestylejs/ani-core'
import { createAniRef, createTimeline } from '@freestylejs/ani-solid'
const node = a.ani({ to: { translateX: 300 }, duration: 0.5 })
export function Box() {
const timeline = createTimeline(() => a.rafTimeline(node))
const { ref, style } = createAniRef(timeline, {
initialValue: { translateX: 0 },
})
return (
<div
ref={ref}
style={style()}
onPointerDown={(event) => {
void timeline.play({ from: { translateX: event.clientX } })
}}
/>
)
}createAniStates returns { ref, style, state, transitionTo, machine }, and state is an accessor: state().
Svelte
useAniRef returns { action, style } instead of [action, controller]. Use use:action and pass style to the element.
// 1.x
const [refAction, controller] = useAniRef({
timeline: myTimeline,
initialValue: { translateX: 0 },
events: { onPointerdown: (ctx, event) => ctx.play({ from: { translateX: event.clientX } }) },
})
// <div use:refAction /><script>
import * as a from '@freestylejs/ani-core'
import { useAniRef, useTimeline } from '@freestylejs/ani-svelte'
const timeline = useTimeline(() =>
a.rafTimeline(a.ani({ to: { translateX: 300 }, duration: 0.5 }))
)
const { action, style } = useAniRef(timeline, {
initialValue: { translateX: 0 },
})
</script>
<div
use:action
{style}
on:pointerdown={(event) => timeline.play({ from: { translateX: event.clientX } })}
/>useAniStates returns { action, style, state, transitionTo, machine } instead of [action, { state, timeline }, transitionTo], and state is a store: $state.
Behavior changes
These changes compile and run, so the type checker and the codemod cannot find them. Check each one.
Delay unit
The delay of play() and transitionTo() is in seconds, on RAF and WAAPI. See Delay held in variables.
Reduced motion
Animations honor prefers-reduced-motion by default. With the system preference on, rafTimeline, waapiTimeline, rafGroup, follow and the timelines made by createStates apply the policy 'jump': play() or set() validates as usual, then the run completes at once in its end state (follow on the next frame). Bound targets are written, listeners are notified once, and the promise resolves 'ended'. The exception is an endless run — a tree containing loop(..., Infinity), whose duration is Infinity; repeat: Infinity on a finite tree is not endless — which holds its first end state with status PAUSED and keeps its promise pending instead of resolving 'ended'.
Opt out per timeline with the reducedMotion option. Keep 'ignore' for motion that carries information (a progress indicator), or shorten instead of skipping with { maxDuration }.
import * as a from '@freestylejs/ani-core'
const node = a.ani({ to: { width: 100 }, duration: 1 })
// essential motion: unchanged behavior
const progress = a.rafTimeline(node, { reducedMotion: 'ignore' })
// a short transition instead of an instant change
const panel = a.waapiTimeline(node, { reducedMotion: { maxDuration: 0.2 } })The preference lives on the clock. To change it for every timeline on a clock, create the clock with createClock({ reducedMotion: false }) and pass it as { clock }. The policy is separate from the clock: pass reducedMotion to rafTimeline, waapiTimeline, rafGroup, follow or createStates, which forwards it to every state's timeline.
rafGroup and follow honor the same preference: rafGroup accepts 'jump', 'ignore' and { maxDuration }; follow accepts 'jump' and 'ignore' (a spring has no duration). Only drive() does not read the preference: a driven run is progress-driven, so the user controls the motion. Where matchMedia does not exist (servers, plain test environments) the preference is off, so existing tests keep their timings. See Reduced motion.
Commit phase
update only computes. Bound styles are written and onUpdate callbacks run in the commit phase, after every timeline on the clock has computed. A frame notifies once: the frame in which a run ends notifies once with ENDED and the final value, and a frame that wraps a repeat notifies once with the post-wrap value.
Code that depended on the 1.x order needs a look:
- A callback that reads another timeline's
valuesees that timeline's value of the same frame, because every timeline has computed before any callback runs. - Code that mixed layout reads with style writes should move the reads to
clock.read(fn). See clock. play(),seek()andreset()still notify synchronously.
import * as a from '@freestylejs/ani-core'
const timeline = a.rafTimeline(a.ani({ to: { opacity: 1 }, duration: 0.3 }))
const element = document.createElement('div')
// styles are written before the listeners of the same notification run
timeline.bind(element)
timeline.onUpdate(({ status }) => {
if (status === 'ENDED') element.dataset.done = 'true'
})Promises that never reject
play() and transitionTo() return a Promise<RunResult> that resolves once per run with 'ended', 'interrupted', 'skipped', 'reset' or 'disposed'. The promise never rejects, so .catch() handlers and try blocks around await never run. An invalid config still throws synchronously, before any promise exists.
import * as a from '@freestylejs/ani-core'
const timeline = a.rafTimeline(a.ani({ to: { opacity: 1 }, duration: 0.3 }))
export async function reveal(): Promise<boolean> {
const result = await timeline.play({ from: { opacity: 0 } })
return result === 'ended' // not 'interrupted', 'reset' or 'disposed'
}Treat 'interrupted' as normal control flow: a second play() over a running timeline settles the first run with it.
1.x call shapes throw
Un-migrated JavaScript fails loudly. Each of these throws a TypeError that names the 2.0 form, before anything changes:
a.sequence(children, timing) // use { timing }
a.parallel(children, timing, id) // use { timing, id }
a.loop(node, count, timing) // use { timing }
a.stagger(children, 0.1) // use { offset: 0.1 }
a.ani(props, id) // put id into props
a.delay(1, id) // use { id }
a.rafTimeline(node, clock) // use { clock }
timeline.play(config, false) // use { ...config, interrupt: false }
timeline.play({ from, context }) // context is removed
timeline.play({ from, propertyResolver }) // use bind(target, { resolver })
timeline.reset(true) // reset() takes no arguments
machine.transitionTo(state, config, false) // use { ...config, interrupt: false }
a.createStates({ initialFrom }) // use initialValueA positional argument is never guessed. Four changes cannot be detected at runtime: the delay unit, reduced motion, the commit phase and the loop overrides.
Reset always notifies
reset() notifies subscribers once with status IDLE and an empty state, and the non-notifying reset(false) is gone. See Reset arguments.
Error message prefixes
Messages are prefixed with the name of the API that threw. The clock prefix changed from [AnimationClock] to [Clock], and the new TypeErrors name the 2.0 form. Code or tests that match message text break. Match errors by class (TypeError, RangeError) or by AniError.code instead.
Loops and overrides
keyframes and durations hold one entry per ani or delay node as written in the tree. The child of a loop is one entry, whatever the loop count, and the entry applies to every iteration. In 1.x a loop was written out as copies and every iteration had its own entry. Remove the extra entries from arrays you pass to looped trees. loop(child, Infinity) is now allowed. See loop.
WAAPI runs
A waapiTimeline can play one element or a list, with a per-target stagger. Playing over a running timeline commits the current styles of every old animation and cancels it, so the element continues from the value it showed. from is a record of CSS properties. See waapiTimeline.
Engine corrections
Several 1.x behaviors were defects and are fixed in the same release. They are not API changes, but they can change what you see. Each row links to the page that describes the 2.0 behavior.
| Area | 2.0 behavior |
|---|---|
| Overlapping segments | The segment that started most recently produces the value. See rafTimeline. |
| Invalid configs | play() throws synchronously and leaves the timeline untouched. See Validation. |
| Values | value is a frozen snapshot; do not mutate it. All onUpdate callbacks of one notification receive the same read-only state. |
| Interruption | play() over a PLAYING timeline inherits its velocity. Pass velocity: { x: 0 } to start from rest. |
| Springs | dynamicSpring integrates in fixed substeps and ends when settled, or at its duration. See timing. |
| Frame time | Lag smoothing follows the wall clock under jank and resumes after a suspension. maxDeltaTime is unlimited by default. See clock. |
| State machines | Replaced timelines are disposed; an unknown state throws a RangeError. See createStates. |
| Nodes | ani, delay and stagger throw a RangeError for negative or non-finite durations and offsets. loop needs an integer count >= 0 or Infinity. |
| Server rendering | play() without requestAnimationFrame does not throw. See SSR. |
Checklist
Work through the list before you ship.
- Commit, then run the codemod once on every directory with ANI code.
- Read every warning and fix it with the matching section above.
- Search for
delayin configs held in variables and divide by 1000. - Run
tsc --noEmitand replace every removed name from the table. - Replace
instanceofchecks withisRafTimelineandisWaapiTimeline. - Replace
eventsandEventManagerwith framework handlers, and call the timeline from them. - Update every binding call:
useAniRef,useAniStatesand their Vue, Solid and Svelte equivalents. - Put
stylefrom the bindings on the elements, and create owned timelines withuseTimeline. - Check code that awaits
play(): handle'interrupted', drop the.catch(). - Decide the reduced motion policy of every essential animation.
- Review
onUpdatecallbacks for commit-phase ordering and for theIDLEnotification ofreset(). - Remove per-iteration entries from
keyframesanddurationsof looped trees. - Match errors by class or
AniError.code, not by message text. - Run your tests with
createManualClockandresetDefaultClock. See Testing.
Related Components
- Reduced motion - Policies and testing.
- Testing - Deterministic tests with a manual clock.
- Large-scale usage - Ownership and cost rules.
rafTimelineandwaapiTimeline- The two engines.