Use CSS animations for state changes, simple loops, entry and exit transitions, and basic scroll reveals; use GSAP when you need sequencing, playback control, pinned scroll scenes or values CSS cannot animate. In 2026 CSS covers far more ground than it did two years ago, so the honest answer for most projects is “both”, with CSS as the default and GSAP where the choreography gets complicated.

That line has moved. Scroll-driven animations shipped in Safari 26, same-document View Transitions reached every major engine, and @starting-style finally made entry animations from display: none possible in plain CSS. This guide walks through what modern CSS can now do on its own, where GSAP still earns its place, and how to decide.

GSAP vs CSS at a Glance

FeatureCSS (late 2026)GSAP
Hover, focus, state transitionsExcellent, no JavaScriptWorks, usually overkill
Entry and exit from display: none@starting-style + allow-discrete, all enginesYes, via tweens and callbacks
Staggering many elementssibling-index(), newly in all enginesstagger with from, grid and easing options
Sequencing and timelinesManual delays, no shared timelinegsap.timeline() with labels and position parameters
Play, pause, reverse, seekLimited (animation-play-state, class toggles)Full control of any tween or timeline
Scroll-linked progressanimation-timeline: scroll() / view(), not in Firefox stableScrollTrigger, every modern browser
Pinning and snapping scroll scenesposition: sticky and CSS scroll snap, no timeline snappingpin, snap to labels or progress
Scroll callbacksNoneonEnter, onLeave, onUpdate and more
Layout and page transitionsView Transitions APIFlip plugin
Custom easinglinear() for bounces and springsBuilt-in eases plus CustomEase
SVG paths, canvas, WebGL, numbersCSS properties onlyAny numeric JavaScript value
Runs off the main threadYes, for transform and opacityNo, updates from JavaScript
CostNothing to loadA small library, free including every plugin

What Modern CSS Handles Well

State Changes and Loops

Hovers, focus rings, menu toggles and spinners are still CSS territory. A transition or a @keyframes loop is less code, needs no script, and keeps working if your JavaScript fails to load.

.button {
  transition: transform 0.2s ease, background-color 0.2s ease;
}

.button:hover {
  transform: translateY(-2px);
  background-color: #c8ff00;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

.spinner {
  animation: spin 1s linear infinite;
}

@media (prefers-reduced-motion: reduce) {
  .button,
  .spinner {
    transition: none;
    animation: none;
  }
}

Entry and Exit Animations with @starting-style

Animating an element in from display: none used to need JavaScript, because the browser had no “before” style to transition from. @starting-style supplies it, and transition-behavior: allow-discrete (written here as the allow-discrete keyword) lets display flip at the right moment so the exit animation plays too. Both work in Chrome, Edge, Firefox and Safari.

.panel {
  display: none;
  opacity: 0;
  transform: translateY(12px);
  transition:
    opacity 0.3s ease,
    transform 0.3s ease,
    display 0.3s allow-discrete;
}

.panel.is-open {
  display: block;
  opacity: 1;
  transform: none;
}

@starting-style {
  .panel.is-open {
    opacity: 0;
    transform: translateY(12px);
  }
}

Staggers and Custom Easing

Two smaller additions close gaps that used to push people toward a library. sibling-index() became available in all three engines in August 2026, so a stagger no longer needs one nth-child rule per item. The linear() easing function, now widely available, draws any curve as a series of points, which makes bounces and spring-like settles possible in plain CSS.

@media (prefers-reduced-motion: no-preference) {
  .card {
    animation: rise 0.5s both;
    animation-delay: calc((sibling-index() - 1) * 80ms);
    animation-timing-function: linear(0, 0.6 30%, 1.08 55%, 0.97 75%, 1);
  }
}

@keyframes rise {
  from { opacity: 0; transform: translateY(24px); }
}

Because sibling-index() is so new, an older browser drops the delay and every card animates at once, which is an acceptable fallback.

Scroll-Driven Animations

animation-timeline ties a CSS animation to scroll instead of time. scroll() follows a scroll container’s progress; view() follows an element’s journey through the viewport. They run in Chrome and Edge 115+ and Safari 26+, but Firefox still ships them only in preview builds, so they are not yet Baseline. Wrap them in @supports so content stays visible everywhere else.

@media (prefers-reduced-motion: no-preference) {
  @supports (animation-timeline: view()) {
    .reveal {
      animation: fade-up linear both;
      animation-timeline: view();
      animation-range: entry 0% entry 100%;
    }
  }
}

@keyframes fade-up {
  from { opacity: 0; transform: translateY(24px); }
}

The free CSS Scroll Reveal effect is built exactly this way, with no JavaScript at all.

View Transitions

The View Transitions API animates between two DOM states: you change the page inside document.startViewTransition() and the browser crossfades it, morphing any element that shares a view-transition-name. Same-document transitions work in every major engine since Firefox 144 (October 2025). Cross-document transitions, which animate between separate pages of a multi-page site, work in Chrome, Edge and Safari 18.2+ but not yet in Firefox.

function updateView(applyChange) {
  const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
  if (!document.startViewTransition || reduce) {
    applyChange();
    return;
  }
  document.startViewTransition(applyChange);
}

For a filterable grid or a list-to-detail change, this is now a strong native option.

Where GSAP Still Earns Its Place

GSAP is not a replacement for CSS; it is what you reach for when the animation stops being a single property change and becomes choreography.

Timelines and Playback Control

CSS has no shared timeline. Chaining steps means hand-calculated delays, and reversing halfway through means toggling classes and hoping the timing lines up. A GSAP timeline sequences steps relative to each other and can be played, paused, reversed or scrubbed at any point.

const mm = gsap.matchMedia();

mm.add("(prefers-reduced-motion: no-preference)", () => {
  const tl = gsap.timeline({ paused: true });

  tl.to(".modal", { autoAlpha: 1, duration: 0.3 })
    .from(".modal-content", { y: 20, duration: 0.3 }, "-=0.1")
    .from(".modal-buttons > *", { opacity: 0, y: 10, stagger: 0.08 });

  openButton.addEventListener("click", () => tl.play());
  closeButton.addEventListener("click", () => tl.reverse());
});

Interrupt it halfway and reverse() runs backwards from wherever it is. That behaviour is what makes interactive sequences feel solid.

Pinned Scroll Scenes with ScrollTrigger

CSS scroll-driven animations handle “progress follows scroll”. ScrollTrigger adds everything around it: pinning a section with automatic spacing, snapping to timeline labels, callbacks as sections enter and leave, and recalculating on resize. It also behaves the same in Firefox today.

gsap.registerPlugin(ScrollTrigger);

const mm = gsap.matchMedia();

mm.add("(prefers-reduced-motion: no-preference)", () => {
  const tl = gsap.timeline({
    scrollTrigger: {
      trigger: ".story",
      start: "top top",
      end: "+=2000",
      pin: true,
      scrub: 1,
      snap: { snapTo: "labels", duration: 0.4 },
      onLeave: () => document.body.classList.add("story-done")
    }
  });

  tl.addLabel("one")
    .from(".story .panel-2", { yPercent: 100 })
    .addLabel("two")
    .from(".story .panel-3", { yPercent: 100 })
    .addLabel("three");
});

You can approximate pieces of this with position: sticky and CSS scroll snap, but not the combination. The Horizontal Scroll Section and Scroll Image Sequence effects are good examples of pinned scenes that sit squarely in GSAP territory. For reveals, scrubbing, pinning and callbacks step by step, plus a closer look at CSS scroll-driven animations against ScrollTrigger, see How to Animate on Scroll with GSAP ScrollTrigger.

Values CSS Cannot Animate

CSS animates CSS properties. GSAP animates any numeric value: a counter, an SVG path, a canvas particle, a Three.js camera.

const stats = { value: 0 };

gsap.to(stats, {
  value: 1000,
  duration: 2,
  ease: "power2.out",
  onUpdate: () => {
    counter.textContent = Math.round(stats.value);
  }
});

gsap.to(mesh.position, { x: 5, y: 2, duration: 1 });

Plugins extend this further: MorphSVG for shape morphing, SplitText for character and line animation, ScrambleText for decoding text like the Text Scramble Decode effect.

Layout Animation Where View Transitions Stop

View Transitions snapshot the page and animate images of the old and new states. By default that snapshot overlay also captures clicks until the transition ends (::view-transition { pointer-events: none; } opts out), and the moving pieces are pictures rather than live elements. GSAP’s Flip plugin animates the real elements instead, so the change can be interrupted, nested inside a timeline or tied to scroll. The FLIP Layout Morph effect shows the kind of layout change where that matters. For a simple one-off state swap, View Transitions are often enough.

Responsive Choreography

gsap.matchMedia() creates animations when a media query matches and reverts them cleanly when it stops matching, including any ScrollTrigger pins. That is how you ship a pinned horizontal gallery on desktop and a plain stacked list on mobile without leftover inline styles. The same mechanism handles prefers-reduced-motion, as in the examples above.

Performance: What Actually Matters

The old claim that “CSS is always faster” is half right, and the half that is right matters.

Where CSS genuinely wins:

  • CSS animations and transitions of transform and opacity can run on the compositor thread. If the main thread is busy parsing JavaScript or running a heavy framework render, they keep moving. GSAP updates styles from JavaScript on the main thread, so a long task can make it stutter.
  • CSS scroll-driven animations of those properties can also run off the main thread, which makes them very smooth for simple parallax and reveals.
  • There is nothing to download or parse.

Where GSAP holds its own:

  • It animates the same cheap properties, and for typical interface work both hold a smooth frame rate.
  • It batches its reads and writes on a single ticker, which keeps dozens of coordinated tweens efficient.
  • ScrollTrigger handles refresh and resize logic that you would otherwise write by hand.

What matters more than the tool:

  • Animate transform and opacity; avoid animating width, top or box-shadow on many elements.
  • Keep long JavaScript tasks off the main thread during animation, whichever tool you use.
  • Respect prefers-reduced-motion in every animation, CSS or GSAP.

Choose on maintainability and capability first, then measure if a specific animation feels heavy.

The Decision Framework

Reach for CSS when:

  • The animation is a response to state: hover, focus, open, closed
  • It is a simple loop or a one-step entry or exit
  • It is a basic scroll reveal or progress bar and a static fallback in Firefox is fine
  • You want a crossfade or simple morph between two DOM states (View Transitions)
  • You want zero JavaScript for that feature

Reach for GSAP when:

  • Several elements must move in a coordinated sequence
  • The user can interrupt, reverse or scrub the animation
  • A scroll scene needs pinning, snapping or callbacks
  • You are animating SVG paths, canvas, WebGL or numbers
  • Behaviour must be the same in every modern browser today
  • Animations must be created and torn down at breakpoints

Use both. A common, sensible split is CSS for interface micro-interactions and GSAP for hero sequences, scroll storytelling and anything interactive. The two do not conflict, as long as CSS transitions and GSAP tweens are not animating the same property on the same element.

If you are also weighing GSAP against other JavaScript libraries, GSAP vs Anime.js vs Motion compares them on the same terms.

Getting Started with GSAP

GSAP is free, including every plugin, under GreenSock’s Standard “No Charge” GSAP License; the former Club plugins have been free since 3.13. Read the official licence for the binding terms. Install it with npm install gsap, or load it from a CDN:

<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/gsap.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.15.0/dist/ScrollTrigger.min.js"></script>
gsap.registerPlugin(ScrollTrigger);

const mm = gsap.matchMedia();

mm.add("(prefers-reduced-motion: no-preference)", () => {
  gsap.from(".hero-text", {
    y: 50,
    opacity: 0,
    duration: 1,
    scrollTrigger: {
      trigger: ".hero",
      start: "top 80%"
    }
  });
});

The GSAP documentation is the place to go deeper.

Conclusion

CSS in 2026 is a genuinely capable animation tool: entry and exit transitions, staggers, custom easing, scroll-driven timelines and View Transitions all work without a line of JavaScript, most of them in every major browser. That shifts the default. Start with CSS.

GSAP is still the right call when an animation becomes choreography: timelines you can control, pinned and snapped scroll scenes, callbacks, and values CSS cannot reach. The best projects use each where it is strongest.


Looking for production-ready GSAP effects? Browse the GSAP effects library for scroll animations, text effects and interactive components you can drop into any project, or start with the scroll animation category.