Declarative vs Imperative Animation in React: Where Each Model Wins
A menu opens because isOpen became true. A notification leaves because React removed it from the tree. A card changes visual state when it receives focus.
Then there is the hero where the heading splits into lines, the image unmasks 180 milliseconds later, the background drifts while the section is pinned, and the sequence changes direction when the user scrolls back.
Both are animation. They are not the same programming problem.
The useful distinction is state-driven motion versus sequence-driven motion.
If application state determines what the animation should resolve to, start declaratively. If timing, sequencing, measurement, or another browser-controlled signal defines the behavior, imperative control often maps more cleanly.
That decision comes before the library choice. Motion has both declarative React APIs and imperative controls such as useAnimate; GSAP is built around imperative tweens and timelines but provides React-specific lifecycle tooling. Declarative does not mean Motion, and imperative does not mean GSAP. They are programming models that different libraries expose in different ways.
Declarative animation describes the visual consequence of state
Consider a sidebar:
<motion.aside
animate={{
x: isOpen ? 0 : "-100%",
opacity: isOpen ? 1 : 0,
}}
/>
The animation is attached to something the application already understands: isOpen.
React owns the state. The animation layer translates a change in that state into visual movement.
You do not need to describe every path the sidebar could take from every previous position. You declare the state it should resolve toward and let the animation system handle the transition.
This model fits naturally when motion follows meaningful interface states:
- open and closed;
- selected and unselected;
- hover, tap, or focus;
- mounted and unmounted;
- expanded and collapsed;
- layout states already represented by component data.
A disclosure panel is a straightforward example:
<motion.div
animate={{
height: expanded ? "auto" : 0,
opacity: expanded ? 1 : 0,
}}
/>
The component has a real domain state: expanded or collapsed. The animation is merely the visible transition between those conditions.
Introducing a timeline, storing it in a ref, then calling .play() and .reverse() would add orchestration to a problem that barely has a sequence.
For ordinary interface feedback, the declarative version is often boring.
Boring is useful when somebody has to maintain the component six months later.
Declarative becomes awkward when state is only pretending to be time
Now imagine an intro sequence:
- reveal a hero image;
- begin the headline before that reveal finishes;
- stagger several metadata items;
- overlap the navigation entrance;
- pause briefly;
- start another visual event at a labelled point.
You can represent every stage as state:
const [phase, setPhase] = useState("intro");
const [imageVisible, setImageVisible] = useState(false);
const [titleVisible, setTitleVisible] = useState(false);
const [detailsVisible, setDetailsVisible] = useState(false);
But those booleans do not describe meaningful application state. They exist because the developer needs a clock.
At that point, React state has become a fairly expensive stopwatch.
The important information in this interaction is not whether the heading is ultimately visible. It is when its movement starts relative to everything else.
That is a different abstraction.
Imperative animation describes what should happen over time
A timeline makes temporal relationships explicit:
const tl = gsap.timeline();
tl.from(image, {
clipPath: "inset(100% 0 0 0)",
duration: 1,
})
.from(
heading,
{
yPercent: 100,
stagger: 0.08,
},
"-=0.55"
)
.from(
meta,
{
opacity: 0,
y: 12,
stagger: 0.06,
},
"-=0.25"
);
The heading begins before the image finishes. The metadata follows while the heading is still moving. Changing the choreography means adjusting relationships on one shared temporal axis.
The timeline is carrying information that a collection of end states would obscure.
This is where imperative animation tends to earn its complexity: when the sequence itself is part of the behavior.
Rich hero choreography is one example. Coordinated SVG sequences, route transitions, scroll-linked storytelling, and animation systems responding to measurements or browser events can create the same requirement.
The condition matters more than the category. A scroll effect does not become imperative merely because scrolling is involved. It becomes a stronger candidate when several elements need coordinated progress, relative timing, reversible sequences, or mutable control that is clearer as a timeline than as component state.
Motion and GSAP do not map one-to-one onto the two models
It is tempting to reduce the choice to:
Motion = declarative
GSAP = imperative
That is useful as a rough introduction and increasingly inaccurate as architecture.
Motion's React components work especially well for state-driven animation:
<motion.button
animate={{ scale: active ? 1.05 : 1 }}
whileTap={{ scale: 0.97 }}
/>
But Motion also exposes useAnimate, which provides scoped manual animation controls, sequences, selectors, and automatic cleanup when the component is removed.
function List() {
const [scope, animate] = useAnimate();
async function reveal() {
await animate(
"li",
{ opacity: 1, y: 0 },
{ delay: stagger(0.06) }
);
await animate(
".footer",
{ opacity: 1 },
{ duration: 0.3 }
);
}
return (
<ul ref={scope}>
...
</ul>
);
}
That is imperative sequencing inside Motion.
GSAP travels in the other direction. Its animation model is imperative, but its React tooling addresses component scoping and cleanup instead of asking the developer to manage every lifecycle edge manually. GSAP's documentation recommends the React-specific useGSAP() hook for this job and describes gsap.context() as the lower-level mechanism for grouping and reverting animations.
The architectural question therefore survives whichever library you prefer:
Where does the source of truth for this behavior belong?
A useful dividing line: state or coordination
Several examples make that boundary easier to see.
Disclosure panel: declarative
A disclosure has a meaningful state:
expanded = true;
Animation follows that state.
<motion.div
animate={{
height: expanded ? "auto" : 0,
opacity: expanded ? 1 : 0,
}}
/>
There is little value in constructing a timeline unless the disclosure itself has unusually rich choreography.
Button feedback: declarative
A pressed button does not need a temporal control system. It needs to acknowledge input and resolve to a known visual state.
<motion.button whileTap={{ scale: 0.96 }}>
Save
</motion.button>
Component state or gesture state already describes the behavior.
Coordinated hero reveal: imperative
If five elements share a carefully designed entrance and the important decisions are overlaps, stagger gaps, labels, and playback control, a sequence is often easier to read than several intermediate React states.
Drag interaction: depends on ownership
A card whose motion maps directly onto a gesture API may fit comfortably into declarative animation.
A drag system with velocity transfer, collision logic, snapping, external measurements, or a larger timeline may need more explicit control.
“Drag” is not the architecture. The data flow is.
Coordinated pinned storytelling: often imperative or hybrid
A pinned section might contain a stable claim beside changing media. If each state simply maps to a scroll threshold, declarative state can remain perfectly reasonable.
Once the behavior depends on shared progress, reversible choreography, measured offsets, or several elements moving along one scroll-controlled sequence, imperative control becomes easier to justify.
The model follows the coordination problem, not the visual genre.
React is allowed to work with imperative systems
React encourages declarative rendering. It does not require every browser interaction to become React state.
Refs and effects exist partly because applications need to coordinate with systems outside React's render cycle: media players, canvas contexts, animation engines, observers, pointer data, and other browser APIs.
A React component using GSAP can keep that boundary narrow.
Using GSAP's React hook, the setup can look like this:
import { useRef } from "react";
import gsap from "gsap";
import { useGSAP } from "@gsap/react";
gsap.registerPlugin(useGSAP);
function Hero() {
const root = useRef(null);
useGSAP(
() => {
gsap.from(".title", {
yPercent: 100,
duration: 0.8,
});
},
{ scope: root }
);
return (
<section ref={root}>
<h1 className="title">Build the sequence.</h1>
</section>
);
}
GSAP's React integration scopes selector work and handles animation cleanup through its context system, rather than leaving timelines orphaned when the component disappears.
React still owns the component tree. GSAP manages the transient visual progression.
Those responsibilities can coexist without either pretending to own the other's job.
The real problem is unmanaged imperative animation
Imperative animation gets a bad reputation when the implementation has no lifecycle boundary.
The trouble usually looks less dramatic than a broken tween:
- selectors reaching outside the component;
- timelines recreated after every render;
- ScrollTriggers surviving route changes;
- pointer listeners added repeatedly;
- dimensions measured once and treated as permanent;
- animations continuing against nodes that no longer exist.
None of these failures are caused by the word “imperative.” They come from unclear ownership.
A useful imperative setup should make it obvious:
- who creates the animation;
- what DOM region it may affect;
- what causes it to rebuild;
- what happens on resize;
- what gets reverted on unmount;
- who owns any listeners, observers, or external subscriptions.
If those answers are difficult to find, the animation architecture is already telling you something.
Do not put every animation value in React state
Consider a pointer-following element.
You could do this:
const [position, setPosition] = useState({ x: 0, y: 0 });
function onPointerMove(event) {
setPosition({
x: event.clientX,
y: event.clientY,
});
}
There are cases where this is harmless. There are many where the coordinates do not represent application state and have no reason to cause React renders on every pointer update.
Transient visual values often belong somewhere else:
- a ref;
- a Motion value;
- an animation engine;
- a canvas state object;
- a shader uniform.
React needs to know that the modal is open. It usually does not need to know that a decorative cursor is currently at x = 487.2.
The same distinction becomes more obvious in graphics work. A WebGL render loop may update uniforms every frame while React remains responsible for larger state changes such as which scene or product is active.
The UI state and rendering state are related. They do not need to be identical.
Imperative does not mean writing your own render loop
Choosing imperative animation also does not mean dropping directly to:
requestAnimationFrame(tick);
An animation library can still handle interpolation, easing, scheduling, cancellation, and sequencing.
The distinction is about how behavior is expressed.
This:
<motion.button whileTap={{ scale: 0.96 }} />
describes a visual state associated with an interaction.
This:
animate([
[title, { y: 0, opacity: 1 }],
[image, { scale: 1 }, { at: "-0.3" }],
]);
describes a sequence.
Either can live inside the same application. Depending on the library, they can even live inside the same animation system.
There is no architectural prize for forcing the tooltip and the scroll narrative to use identical control models.
Hybrid animation is often the production model
More complicated React interfaces frequently divide ownership rather than choosing one philosophy for the entire codebase.
React might own:
- whether a modal exists;
- which gallery item is active;
- which route is selected;
- what data is displayed;
- whether a section is expanded.
The animation system can manage:
- tween progress;
- timeline position;
- spring values;
- transient pointer coordinates;
- scroll-linked transforms.
Imagine a product gallery.
React owns:
const [activeImage, setActiveImage] = useState(0);
When activeImage changes, an animation system can manage the visual transition between the old and new media.
State explains what changed.
Animation explains how that change unfolds visually.
That division is often cleaner than asking React to represent every frame of the transition or asking an animation timeline to become the source of truth for application data.
Next.js adds a browser boundary, not a new animation philosophy
In a Next.js application, code that directly depends on DOM nodes, window, pointer position, layout measurements, canvas, or WebGL needs to execute on the client side.
That does not mean an entire page has to become a client component because one heading moves.
Keep the interactive boundary as small as the architecture allows:
"use client";
import { useRef } from "react";
A server-rendered page can contain a focused client component responsible for the interaction.
This becomes especially relevant with creative effects because their dependencies are often highly local. One section may need GSAP. Another might use Motion. A WebGL hero may carry a considerably heavier rendering stack while the surrounding page remains ordinary server-rendered content.
The client boundary should follow the browser dependency, not the visual ambition.
Declarative and imperative animation accumulate complexity in different places
Neither model eliminates complexity. It changes where you pay for it.
Declarative animation tends to become difficult when state relationships multiply.
A component begins with open and closed. Later it gains loading, focused, disabled, dragging, exiting, selected, and several variants inherited from a parent. At some point the transition code is simple, but the visual state machine is not.
Imperative animation tends to become difficult when lifecycle and ownership become unclear.
A timeline may read beautifully while referring to stale measurements, dead elements, duplicated listeners, or scroll triggers that survived navigation.
The review questions should reflect those different risks.
For declarative animation, ask:
- Does each animated state correspond to something meaningful in the interface?
- Are variants clarifying the component or hiding an accidental state machine?
- Could another developer understand the visual behavior without tracing several layers of inherited state?
For imperative animation, ask:
- Who creates the sequence?
- When does it rebuild?
- Are selectors scoped?
- What happens when content dimensions change?
- What gets cleaned up?
- Are scroll, resize, pointer, and observer subscriptions disposed correctly?
The better abstraction is often the one whose failure modes remain visible.
Reduced motion belongs above the API choice
Neither model becomes accessible because its syntax is elegant.
A declarative animate prop can still create motion that is uncomfortable. A perfectly scoped imperative timeline can still ignore keyboard users, touch input, or prefers-reduced-motion.
Reduced motion also should not automatically mean “the same animation, but slower.”
A better alternate behavior might:
- remove a large translation while retaining a simple opacity transition;
- reveal scroll-driven content immediately;
- replace a pointer-following treatment with a static state;
- skip decorative sequencing;
- remove the animation entirely.
The final static state matters too. Important content should remain comprehensible when animation is unavailable.
Accessibility decisions sit above declarative versus imperative because both models can express good behavior or bad behavior equally well.
Performance depends on the work being performed
A declarative API is not automatically cheap. An imperative API is not automatically expensive.
Animating a transform on one element and repeatedly measuring layout across dozens of descendants are different workloads, regardless of which syntax initiated them.
For production animation, inspect the actual work:
- Are layout measurements happening repeatedly?
- How many DOM nodes change?
- Does animation continue while the section is offscreen?
- Are pointer events causing unnecessary React renders?
- Does a canvas redraw continuously?
- Does a WebGL scene need the device's full pixel ratio?
- Are listeners and observers cleaned up?
- Does mobile receive a simpler behavior where the richer one provides little value?
The code style does not answer those questions.
The browser does.
A practical decision table
| Situation | Good starting point | Why |
|---|---|---|
| Open/closed component state | Declarative | Motion follows meaningful UI state |
| Hover, tap, focus feedback | Declarative | Interaction maps cleanly onto visual states |
| Enter/exit transitions | Declarative | Presence is usually already represented by the component tree |
| Shared component variants | Declarative | Several descendants can resolve from the same state |
| Rich hero choreography | Imperative | Relative timing and overlaps become first-class information |
| Long coordinated sequences | Imperative | A shared timeline usually represents the behavior more clearly |
| Scroll-linked animation with simple state thresholds | Declarative or hybrid | Scroll can update meaningful states without requiring a timeline |
| Scroll choreography with shared progress and sequencing | Imperative or hybrid | Several elements may need one controllable temporal axis |
| Gesture-driven component feedback | Declarative | Gesture state often maps directly to a visual state |
| Gesture system with measurements, inertia, or wider coordination | Hybrid or imperative | Transient control may exceed useful React state |
| UI state triggering a sophisticated visual sequence | Hybrid | React owns the decision; the animation system owns progression |
| Canvas or WebGL integrated with React UI | Hybrid | React and the render loop usually need different state frequencies |
“Starting point” matters. None of these are language rules.
The purpose of the table is to identify where the information naturally lives before choosing an animation API.
Where editable-source interaction patterns fit
The first version of an animation rarely encounters every production condition.
A hero moves into a different stacking context. The mobile breakpoint arrives sooner than the demo expected. A pointer interaction needs a touch replacement. A scroll sequence has to survive route changes. The original easing feels wrong beside the rest of the product.
At that point, the useful question is not only whether the interaction began declaratively or imperatively. It is whether the implementation can be adapted where those decisions actually live.
Hyperiux Vault takes a source-first approach to that problem. Effects are added to the project as editable source files, and dependencies are scoped to the selected effect rather than requiring one animation engine across the catalogue. Current documentation lists effects that can depend on tools including Motion, GSAP, canvas utilities, Three.js, React Three Fiber, and browser APIs depending on the interaction.
That matters for this particular architectural decision because the control model remains visible.
If a project needs a different breakpoint, cleanup path, timing relationship, DOM structure, mobile fallback, or animation engine, the team can inspect and change the implementation rather than treating the interaction as a sealed runtime.
CTA: Inspect Vault's source-first workflow
Choose ownership before choosing syntax
Declarative animation is strongest when motion is the visual consequence of meaningful application state.
Imperative animation becomes useful when timing, sequencing, measurement, or another continuously changing signal contains the important information.
Many production interfaces need both.
Let React own durable application state. Let the animation layer manage transient visual progression when doing so makes the behavior easier to express and maintain. Keep the lifecycle boundary between them explicit.
Once those responsibilities are clear, declarative and imperative animation stop looking like rival philosophies.
They become two models for two different kinds of information.
Top comments (0)