When CSS Animation Is Enough and When React Needs a Library

Learn where CSS ends and animation libraries begin by matching the tool to the interaction- using CSS for visual states and Motion or GSAP when gestures, layout, presence, or complex choreography require more control.

css-animation-featured
Hitesh Bhardwaj

Hitesh Bhardwaj

Sr. Developer

Read Time: 9 mins

Category: Engineering

Share this Article:

Use CSS when an animation is local, style-driven, and does not need runtime orchestration. In React, add an animation library when the interaction needs capabilities such as gesture handling, dynamic layout animation, coordinated sequencing, or programmatic control that native CSS and View Transitions cannot express cleanly.

The useful boundary is not “simple versus complex.” A visually elaborate background loop can still be a self-contained CSS animation. A modest-looking sortable list may need layout measurement, pointer velocity, interruption, and presence handling. The amount of movement tells you surprisingly little about the machinery required.

React also does not need to own the animation simply because state caused something to move. React can decide which state the interface is in while CSS handles how the browser interpolates between those visual states.

The better question is: what has to control the animation?


Start With the Interaction Job, Not the Library

Consider a favourite button. React owns a boolean. That boolean changes a class or data-state; CSS changes the visual state.

<button
  className="favourite"
  data-active={active}
  onClick={() => setActive((value) => !value)}
>
  Save
</button>
.favourite {
  transition:
    transform 180ms ease,
    background-colour 180ms ease;
}

.favourite[data-active="true"] {
  transform: scale(0.97);
}

@media (prefers-reduced-motion: reduce) {
  .favourite {
    transition: none;
  }
}

React triggers the change without controlling a single animation frame. Adding an animation library here would introduce another abstraction without solving a new problem.

If the browser only needs to interpolate between known styles, CSS is usually enough. If React must coordinate elements entering and leaving, preserve continuity during layout changes, follow live pointer input, or synchronize several events on a timeline, the control problem has moved elsewhere.

A useful capability ladder looks like this:

Known visual state change
↓
CSS transition / keyframes
↓
Imperative native browser control
↓
WAAPI / browser APIs
↓
DOM transition between React states
↓
React <ViewTransition>
↓
Presence, layout, gesture, or spring abstraction
↓
Motion or another React animation library
↓
Precise timeline, pinning, scrub, and choreography
↓
GSAP / orchestration engine

Move down the ladder only when the interaction requires another kind of control.


When CSS Animation Is Enough in React

CSS remains the sensible baseline when React changes state and the visual response can be described without continually consulting application logic.

Transitions cover hover, focus, pressed states, disclosure indicators, navigation underlines, colour changes, transforms, and opacity changes. @keyframes extends the same model to sequences such as loading indicators, decorative loops, headline treatments, or ambient motion.

The important condition is not how many keyframes exist. It is whether the sequence can run from known rules without the application needing to redirect it continuously.

This separation keeps component code legible. React can set aria-expanded, render content, and change a class. CSS can rotate the chevron and change the surface treatment. State management and animation interpolation remain separate concerns.

CSS is especially strong when the animation belongs to a visual state rather than a behavioural system:

  • a button responding to hover, focus, or press;
  • an icon rotating when a disclosure opens;
  • a menu underline moving between known states;
  • a decorative background loop;
  • a simple entrance treatment where the element already exists;
  • a reduced-motion override that removes non-essential movement.

Performance is a poor reason to choose by slogan. CSS is not automatically fast, and JavaScript animation is not automatically slow. Rendering cost depends on what changes, how much layout or paint work those changes trigger, and what the rest of the page is doing. MDN similarly treats CSS and JavaScript animation performance as an implementation question rather than a language-level rule. MDN's CSS and JavaScript animation performance guide makes the same distinction.

Property choice matters. A transform or opacity change can often be handled more cheaply than repeatedly changing geometry that forces layout and paint work. The useful performance question is not “Is this CSS?” It is “What work does this animation cause every frame?”


Native Browser APIs Cover More Ground Than They Used To

There is a middle layer between declarative CSS and a full React animation library.

The Web Animations API, commonly referred to as WAAPI, gives JavaScript imperative control over browser-native animation. It fits when the browser can still perform the animation, but JavaScript needs to start, pause, cancel, seek, reverse, or generate it at runtime.

Element.animate() returns an Animation object, so playback can respond to application events without introducing a separate animation engine. MDN documents Element.animate() and the returned Animation instance.

const animation = panel.animate(
  [
    { opacity: 0, transform: "translateY(12px)" },
    { opacity: 1, transform: "translateY(0)" },
  ],
  {
    duration: 240,
    easing: "ease-out",
    fill: "both",
  }
);

animation.pause();

playButton.addEventListener("click", () => {
  animation.play();
});

cancelButton.addEventListener("click", () => {
  animation.cancel();
});

The important part is not that WAAPI can reproduce something CSS could animate. It is that the application now has an imperative handle on the animation without taking over interpolation frame by frame.

WAAPI still leaves plenty of work to the application. It does not solve React presence, shared component state, gesture recognition, dynamic layout animation, or multi-component sequencing on its own. You are responsible for when an animation is created, when it is cancelled, how it behaves on unmount, and what state the interface should end in.

For isolated imperative effects, that trade-off can be ideal. It also prevents a false choice between “CSS only” and “install an animation framework.”


The Native React Line Moved in 2026

Older React animation advice often treats mounting, unmounting, or moving an element as an automatic library threshold. React 19.3 changed part of that calculation.

Released on September 9, 2026, React 19.3 made <ViewTransition> stable. React can now use the browser View Transition API to animate qualifying DOM changes as elements enter, exit, move, resize, update, or participate in shared transitions.

That matters most around presence.

Historically, exit animation exposed a lifecycle mismatch. React wanted to remove a node, while the animation wanted that node to remain around long enough to leave. Motion's AnimatePresence remains a useful abstraction for this problem, but React now has a native path for suitable transitions. Motion documents AnimatePresence as its exit-animation abstraction.

A simplified React 19.3 version looks like this:

import {
  startTransition,
  useState,
  ViewTransition,
} from "react";

export default function Details() {
  const [open, setOpen] = useState(false);

  return (
    <>
      <button
        onClick={() => {
          startTransition(() => setOpen((value) => !value));
        }}
      >
        Toggle details
      </button>

      {open ? (
        <ViewTransition enter="auto" exit="auto" default="none">
          <section>...</section>
        </ViewTransition>
      ) : null}
    </>
  );
}

The feature still has boundaries. The relevant update needs to participate in React's Transition model, and <ViewTransition> currently applies to DOM transitions rather than serving as a universal animation primitive. React also documents specific enter and exit placement rules and a snapshot-based model that will not fit every layout interaction. The React <ViewTransition> reference describes those constraints.

Browser support matters too. The View Transition API is part of Baseline 2025 across current browser versions, but older browsers may still need a non-animated fallback. That fallback should not feel broken. If the transition disappears, the destination state should still be understandable.


CSS can now follow scroll progress too

Native CSS has also expanded into scroll-linked animation.

animation-timeline can associate animation progress with scroll position, while scroll() and view() provide scroll and view-progress timelines. A progress indicator, reveal, or viewport-linked transform may no longer require a JavaScript scroll handler.

The production caveat is significant. MDN currently marks animation-timeline as Limited availability rather than Baseline. MDN's animation-timeline reference should be checked against the browser matrix for the project.

So the decision is not simply “native exists, therefore use native.” Browser support, fallback quality, and the complexity of the interaction still matter.


When a React Animation Library Earns Its Place

A React animation library starts earning its dependency when motion participates directly in component behavior.

Presence is recurring application behavior

One exit transition might fit React's native View Transition model perfectly. A component system with repeated enter, exit, replacement, nested presence, and sequencing behavior may benefit from a dedicated abstraction.

Motion's AnimatePresence detects when children leave the React tree and lets exit animations finish before those elements disappear. That can make recurring presence behavior easier to express directly inside the component model. Motion's AnimatePresence documentation covers that lifecycle.

The question is no longer whether native React can animate an exit. It can.

The question is whether the native API remains clear once presence becomes a repeated system rather than a single transition.

Dynamic layout changes are a different problem

Consider a filterable product grid.

Remove one card and CSS Grid happily calculates the new layout. The difficult part is preserving spatial continuity while the remaining cards move from their old geometry to their new positions.

Motion's layout and layoutId abstractions are designed around this kind of transition. They animate layout changes caused by React renders and can connect related elements across shared layouts. Motion's layout animation documentation describes both layout and shared layout behavior.

The visible movement may look simple. The implementation has to account for geometry that changed after layout was recalculated.

That is a meaningful reason to introduce an abstraction.

Gestures require continuous runtime input

Dragging makes the threshold even clearer.

A drag interaction must respond while the pointer moves. Position, velocity, constraints, cancellation, momentum, and touch behavior may all affect the result. Motion exposes gesture and drag primitives for those conditions rather than asking each component to rebuild the same pointer machinery. Motion's gesture documentation covers hover, tap, pan, drag, focus, and in-view interaction.

A user can also change direction before an animation finishes. The system needs to react to that interruption rather than complete a stale animation because its duration has not elapsed.

Use the abstraction when it removes interaction code your team would otherwise have to own.

Reusable motion state can become infrastructure

A library can also earn its place when animation states repeat across components.

Perhaps several interface elements share the same spring behavior. A parent state coordinates several descendants. A card family uses common enter, hover, selected, and exit states. At that point the animation layer is providing reusable vocabulary rather than simply tweening numbers.

That is a better reason to add a dependency than “this animation looks complicated.”


When You Need More Than a React-Friendly Animation API

Some interactions stop being component-animation problems and become timeline problems.

A product story might keep a section pinned while several scenes change. Text enters at one point, imagery transforms at another, a device mockup changes state halfway through, and the whole sequence follows scroll position.

CSS can move every element involved. The difficult part is coordinating when each event begins, how the playhead follows scroll, when the section pins, and what happens when the viewport changes.

This is where GSAP-style orchestration becomes relevant.

ScrollTrigger exposes triggering, pinning, scrubbing, snapping, and timeline control directly. In React, that power comes with lifecycle work. Timelines and ScrollTriggers need to be scoped and cleaned up when a component unmounts or its setup changes. GSAP's ScrollTrigger documentation documents those controls.

useGSAP(
  () => {
    const timeline = gsap.timeline({
      scrollTrigger: {
        trigger: section.current,
        pin: true,
        scrub: 1,
        start: "top top",
        end: "+=2000",
      },
    });

    timeline
      .from(".claim", { opacity: 0 })
      .to(".product", { scale: 1.15 })
      .to(".proof", { opacity: 1 });
  },
  { scope: section }
);

A heading fading when it reaches the viewport is a scroll-triggered animation.

A section staying pinned while several scenes progress against one controlled playhead is scroll choreography.

Those are different implementation jobs even if both begin with the user scrolling.


CSS vs Native React vs Motion vs GSAP: Decision Matrix

Interaction jobStart withWhyMain caveat
Hover, focus, or press feedbackCSSThe browser interpolates between known visual statesHover cannot be the only input path
Decorative loopCSS keyframesFixed sequence with little runtime coordinationProvide an appropriate reduced-motion state
Imperative playback of an isolated effectWAAPINative animation with JavaScript playback controlYou still own lifecycle and application integration
Simple scroll-progress treatmentCSS scroll timelineNative progress mapping can remove JavaScriptCurrent support remains compatibility-sensitive
React enter/exitReact <ViewTransition> or CSS where lifecycle permitsReact now has a native transition pathRequires the appropriate React transition model and browser support
Shared DOM transitionReact <ViewTransition>Supports continuity between qualifying DOM statesSnapshot model does not fit every interaction
Reordering list or dynamic layoutMotionLayout abstractions handle geometry changes from rendersTest real layout conditions
Drag or gesture interactionMotionPointer state, interruption, constraints, and physics are first-class concernsTouch behavior still needs deliberate design
Pinned, scrubbed scroll narrativeGSAP + ScrollTriggerTimeline, pinning, and scroll control are the core problemRequires lifecycle cleanup and mobile simplification

A single React application can reasonably contain CSS microinteractions beside a Motion-powered draggable component and one GSAP story section. Forcing unrelated interaction jobs through one engine does not make the architecture more coherent.


Performance, Mobile, and Accessibility Are Separate Decisions

Choosing the smallest animation layer does not certify the result.

A CSS animation can still trigger expensive layout or paint work. A library animation can still be efficient. Inspect which properties change, whether layout is measured repeatedly, whether observers and listeners are cleaned up, and whether animation work continues when the effect is no longer visible.

Profile the production build on representative hardware rather than using the tool name as a proxy for performance.

Reduced motion needs a designed state

prefers-reduced-motion should affect the interaction, not merely shorten every duration.

A pinned narrative might become ordinary vertical sections. A parallax image can remain static. A cursor treatment may disappear while the underlying target remains fully understandable. React also recommends respecting reduced-motion preferences when using View Transitions. React's <ViewTransition> reference discusses reduced-motion handling.

The useful question is what the interface should communicate when movement is reduced.

Sometimes the answer is the final visual state. Sometimes the entire interaction model should simplify.

Touch is not smaller desktop input

A cursor interaction built around precise pointer coordinates has no direct equivalent on a touchscreen.

Likewise, a hover state cannot carry essential information if touch users have no way to trigger it. Elaborate desktop pinning may also need a simpler vertical sequence on smaller screens or short viewports.

Responsive motion should adapt the interaction job, not merely scale the transform values.

Next.js adds a client boundary

In Next.js, effects that depend on pointer movement, scroll position, layout measurement, animation timelines, window, canvas, or other browser APIs need client-side execution.

The 'use client' directive defines that boundary. It does not need to be added to every file beneath the component. Keep static page structure on the server where appropriate, then isolate the interactive behavior in the smallest useful client component. Next.js documents the client boundary in its use client reference.

Browser-only WebGL or canvas effects may also justify a dynamic import when the component has no useful server-rendered state.

This is another reason dependency choice should happen at effect level. A CSS hover treatment and a shader-driven background do not belong to the same architectural tier simply because both move pixels.


Where Vault Fits in the Decision

Once the animation layer is chosen, another problem remains: building the interaction around it.

Hyperiux Vault does not replace CSS, WAAPI, Motion, GSAP, or native browser APIs. Different effects use different technical layers according to the interaction. Some patterns rely on React and CSS. Others require Motion, GSAP, or heavier rendering tools. Those requirements remain visible at effect level in the Vault Dependencies documentation.

That matters because the demo is rarely the final implementation.

The timing may need to change beside the rest of the interface. The breakpoint may arrive earlier. A component may sit inside a different stacking context. A desktop effect may need a different touch path. Reduced motion may need a static state rather than a slower version of the same sequence.

Because Vault effects arrive as editable source in the project, those decisions can be changed where they actually live rather than being limited to a predefined configuration surface.

The workflow is therefore less about choosing one preferred animation engine and more about choosing an interaction whose technical footprint matches the job.

A CSS button should be allowed to remain a CSS button. A drag interaction can use Motion when gesture state justifies it. A pinned product story can use GSAP when the timeline is the real implementation problem.

The durable question is smaller than “Which animation library should this React app use?”

Ask what this interaction needs to control. Start at the lightest layer that expresses that cleanly, then add machinery only when the behavior gives you a reason.

Common Questions

Do I need an animation library for React?

No. React can manage state while CSS handles transitions and keyframes. Add a library when presence, dynamic layout, gestures, springs, or coordinated runtime state would otherwise create substantial custom interaction code.

tags:CSSReactAnimation Libraries

Related Blogs

prebuilt-vs-custom
Engineering
September 23, 2026

Prebuilt Interaction Patterns vs Hand-Built Animations in React

Choose between native APIs, animation libraries, and reusable source-first patterns based on how much control, orchestration, and project-specific adaptation the interaction actually requires.

how-to-build-scroll
Engineering
September 23, 2026

How to Build Scroll Animations in React Without Turning the Page Into a Timeline Mess

Build maintainable scroll experiences by giving each section its own trigger, progress source, or scoped timeline instead of coupling the entire page to one master timeline.

hidden-complexity-of-page-transition
Engineering
September 25, 2026

The Hidden Complexity of Page Transitions in the Next.js App Router

Next.js page transitions look simple until routing, Suspense, layouts and accessibility collide. Learn what App Router transitions actually require in production.

how-to-build-reusable-react-animations
Engineering
September 25, 2026

How to Build Reusable React Animation Components Without Making Every Effect Config-Heavy

Build reusable React animation components without prop explosion. Learn what belongs in props, presets, composition, shared defaults, and source-level edits.

native-scroll-timelines
Engineering
September 25, 2026

Native Scroll Timelines Are Here: Does Every Scroll Effect Still Need JavaScript?

CSS scroll-driven animations can replace JavaScript for many scroll effects, but not all. See where native timelines fit, where JS still earns its place, and how to choose.