Puneet Sharma - Frontend Developer & UI Engineer
Puneet Sharma
Frontend Dev & UI Engineer · 16+ yrs · pixel-perfect HTML, React & WordPress

How to Animate CSS Properties with GSAP (With Live Examples)

How to animate CSS properties with GSAP: transforms, color, border-radius, box-shadow, filter, clip-path, CSS variables & SVG strokes — live demos.
How to Animate CSS Properties with GSAP (With Live Examples)

Most GSAP tutorials start with x and y and stop there, which leaves out a real question: which CSS properties can GSAP actually animate, and does it treat them all the same way? The honest answer is no — GSAP handles a plain numeric property like opacity very differently under the hood from a compound value like box-shadow, a shape like clip-path, or a browser-registered custom property like --pct. This post walks through eight real CSS properties one at a time — transforms, color, border-radius, box-shadow, filter, clip-path, a CSS variable, and an SVG stroke — showing exactly what GSAP does with each one and why it works.

Every demo below is live and interactive right here in the article — click the buttons and watch the property change. Each embed also has a Fork & Edit button that opens the exact code, already running, in My Code — FWD Tools' free in-browser editor. Swap a value, hit run again, and watch what changes — that's the fastest way to build real intuition for how GSAP touches CSS.

New to GSAP itself, before its relationship with specific CSS properties? "GSAP Tutorial for Beginners" on this blog covers gsap.to(), timelines, stagger, and ScrollTrigger from the ground up, and the free, click-based GSAP Playground builds on exactly that foundation across 55 lessons.

Why GSAP instead of a CSS transition for these?

A plain CSS transition genuinely animates most of what's below too — this isn't an argument that GSAP is required. What GSAP adds is control: starting, pausing, reversing, or replaying any of these tweens from JavaScript on demand, sequencing several of them precisely with a timeline, and — for a handful of properties below — interpolation a plain transition can't do at all, like tweening a shape's individual polygon points or drawing an SVG path in stroke by stroke. Every demo here is deliberately small, but each one demonstrates a real category of CSS value GSAP has to actually understand to animate correctly.

1. Transform properties — x, y, scale, rotation, skew

The most common case first, because it's the one every other property gets compared against: several transform values combined into one tween.

The rule:

gsap.to('.box', {
  x: 140,
  y: -20,
  scale: 1.3,
  rotation: 25,
  skewX: 8,
  duration: 1,
  ease: 'power2.out'
});

In plain CSS, x, y, scale, rotation, and skewX/skewY would all have to be packed into one transform string by hand, and animating them at different speeds relative to each other would mean juggling that string yourself. GSAP treats each one as its own independent numeric property, combines them into the correct matrix() behind the scenes every frame, and lets each one carry its own value without any string concatenation from you.

Best for: anything involving movement, sizing, or rotation — far and away the most common category of UI animation. Tip: always prefer these transform shorthands over animating top/left/width/height directly — transforms run on the compositor thread and don't trigger layout recalculation the way box-model properties do.

2. Color properties — backgroundColor, color, borderColor

GSAP's core understands color values well enough to interpolate between two completely different-looking colors smoothly, not just fade one out and the other in.

The rule:

gsap.to('.card', {
  backgroundColor: '#e8f9ee',
  color: '#15803d',
  borderColor: '#16a34a',
  duration: 0.6,
  ease: 'power1.out'
});

GSAP parses hex, rgb()/rgba(), hsl(), and named CSS colors, converts both ends to the same color space, and tweens each channel independently — which is exactly what makes a status card's background, text, and border all shift together read as one coherent color change instead of three separate ones landing slightly out of sync.

Best for: status changes, confirmations, and any UI feedback that reads as "this state changed" through color. Tip: keep the color pairs reasonably close in lightness when tweening several color properties together (as this demo does) — wildly different luminance values between start and end can make the mid-tween color look muddy for a frame or two.

3. border-radius — morphing a shape

Each of the four corners is its own value, which means each one can be tweened to a different target — enough to morph a clean square into an organic blob shape.

The rule:

gsap.to('.blob', {
  borderTopLeftRadius: '60% 40%',
  borderTopRightRadius: '40% 60%',
  borderBottomLeftRadius: '40% 60%',
  borderBottomRightRadius: '60% 40%',
  rotation: 8,
  duration: 1.1,
  ease: 'power2.inOut'
});

The shorthand border-radius property itself can't be tweened as one value when the four corners are heading toward different targets, so this splits it into its four longhand properties — borderTopLeftRadius and so on — each animated independently. Each one also accepts the two-value elliptical syntax ('60% 40%' for horizontal/vertical radius), which is what turns a simple rounded square into something that reads as organic rather than mechanically rounded.

Best for: decorative blob shapes, playful loading states, or a card that subtly softens on hover. Tip: combine a small rotation alongside the corner tweens, as this demo does — a pure corner morph with zero rotation can read as strangely static even while it's actively animating.

4. box-shadow — a compound value

Unlike the transform shorthands above, box-shadow genuinely is one single value in CSS — offset, blur, spread, and color all in one string. GSAP still animates it correctly.

The rule:

gsap.to('.card', {
  y: -10,
  boxShadow: '0 22px 34px rgba(99,102,241,0.28)',
  duration: 0.5,
  ease: 'power2.out'
});

GSAP's CSSPlugin parses the whole box-shadow string on both ends of the tween, matches up the corresponding numeric pieces (offset-x, offset-y, blur radius, spread radius, and each color channel), and interpolates each one in place — something a naive string-splice couldn't do reliably, and that a plain CSS transition on box-shadow genuinely does too, just without the pause/reverse/sequence control GSAP adds.

Best for: a card or button "lifting" on hover or focus — pairing a small upward y move with a larger, softer shadow is what sells the depth. Tip: keep the resting-state shadow present (as this demo's idle 0 1px 2px shadow is) rather than starting from none — animating from zero blur/spread to a large one can look like the shadow is "growing into existence" rather than lifting.

5. filter — blur, brightness, grayscale together

Like box-shadow, filter is a compound value — potentially several filter functions chained in one string — and GSAP tweens the whole thing as one property.

The rule:

gsap.to('.tile', {
  filter: 'blur(0px) brightness(1) grayscale(0)',
  duration: 0.9,
  ease: 'power1.out'
});

Both the starting and ending filter strings need the same functions listed, in the same order, for GSAP to match each one up and interpolate its number — blur(6px) pairs with blur(0px), brightness(0.75) with brightness(1), and so on. That's exactly the "focus in" effect used for lazy-loaded images and content reveals across the web: start blurred, desaturated, and dim; land sharp, colored, and at full brightness.

Best for: image reveals, "focus in" content loading effects, and disabling/enabling states (grayscale + reduced brightness reads clearly as "inactive"). Tip: filter is genuinely expensive to render compared to transforms — fine for a handful of elements animating at once, but worth avoiding on large lists or during a scroll-linked animation with dozens of elements.

6. clip-path — a shape-based wipe

Where filter and box-shadow are still fundamentally numbers under the hood, clip-path pushes further — GSAP tweens the individual coordinate points of a polygon.

The rule:

gsap.to('.panel', {
  clipPath: 'polygon(0 0, 100% 0, 100% 100%, 0 100%)',
  duration: 0.9,
  ease: 'power2.inOut'
});

This only works because both the starting polygon (0 0, 0 0, 0 100%, 0 100% — a collapsed sliver on the left edge) and the ending one list exactly four points each. GSAP walks the two point lists in order and tweens each matching x/y pair independently, which is what produces the wipe — the whole rectangle appears to slide open from left to right, entirely through clipping, with no change to the element's actual size or position.

Best for: reveal wipes, image transitions, and any effect where content should appear to be "uncovered" rather than faded or scaled in. Tip: keep the point count identical between the start and end clip-path values — a mismatched number of points is the single most common reason a clip-path tween snaps instead of animating smoothly.

7. A CSS custom property — driving a progress meter

GSAP can tween a CSS custom property directly, as long as the browser has been told, via @property, that the variable holds a number rather than an opaque string.

The rule:

@property --pct {
  syntax: '<number>';
  inherits: false;
  initial-value: 0;
}

gsap.to('.meterFill', {
  '--pct': 82,
  duration: 1.2,
  ease: 'power1.out'
});

Without the @property registration, a browser treats every custom property as an untyped string and can only ever snap between two values, never interpolate them. Registering --pct as a <number> tells the browser (and GSAP) that it's safe to animate numerically, and once that's declared, the meter's width: calc(var(--pct) * 1%) and the percentage label — read back each frame with gsap.getProperty(el, '--pct') in this demo's onUpdate — both stay perfectly in sync with a single source of truth.

Best for: progress meters, gradients driven by a single number, or any case where one animated value needs to feed multiple CSS declarations at once without duplicating the tween. Tip: the @property rule is what makes this animate smoothly at all — skip it and the exact same GSAP code still runs, but the value jumps straight to 82 with no interpolation in between.

8. SVG stroke-dashoffset — drawing a path

Not a CSS box-model property, but a close relative worth knowing: an SVG presentation attribute GSAP animates through the exact same tween syntax as everything above.

The rule:

const length = path.getTotalLength();
gsap.set(path, { strokeDasharray: length, strokeDashoffset: length });

gsap.to(path, {
  strokeDashoffset: 0,
  duration: 0.9,
  ease: 'power1.inOut'
});

stroke-dasharray set to the path's exact length (from the native getTotalLength() method, not a guess) turns the entire stroke into one long dash with no visible gap. stroke-dashoffset, also set to that same length, then shifts that single dash completely out of view. Tweening stroke-dashoffset down to 0 slides the dash back into place, which reads as the line drawing itself in — a checkmark, an underline, a signature, or an icon outline.

Best for: checkmarks, signature or underline reveals, and icon outlines that should feel hand-drawn rather than faded in. Tip: gsap.set() (used here to establish the starting dash state instantly, with no animation) is the same engine as gsap.to() but jumps straight to the given values — reach for it whenever a starting state needs to be set in code rather than baked into the CSS.

Common pitfalls

  • Animating top/left/width/height instead of transforms. Those trigger layout recalculation on every frame; x/y/scale (demo #1) run on the compositor and stay smooth even on weaker devices.
  • Mismatched point counts in a clip-path tween. Demo #6 only animates smoothly because both ends list exactly four polygon points — a different count on either side makes the shape snap instead of morph.
  • Tweening a CSS custom property without @property. Demo #7's meter only interpolates smoothly because --pct is registered as a <number> — skip that and the same GSAP code jumps straight to the end value with no animation in between.
  • Guessing an SVG path's length instead of measuring it. Demo #8's draw-in depends on getTotalLength() being exact — a hand-typed dasharray that's too short or long leaves a visible gap or overlap at the "finished" state.
  • Reaching for filter on a large list of elements. It's the most render-expensive property in this whole list; fine for a handful of elements (demo #5), worth reconsidering for dozens animating at once.

Frequently asked questions

Can GSAP animate any CSS property?

Almost any property with a numeric or interpolatable value, yes — including ones a plain CSS transition also handles, like opacity, colors, and transforms. Where GSAP goes further is compound values it parses itself (box-shadow, filter), shape data (clip-path), and values that need explicit browser registration to animate at all (custom properties via @property). A handful of properties, like display, are inherently non-numeric and can only be toggled, not tweened.

Do I need a GSAP plugin to animate CSS properties?

No — everything in this post uses only core GSAP (CSSPlugin, which ships built in). No plugin registration is needed for any of the eight properties covered here, including the CSS custom property in demo #7.

Why does my CSS custom property jump instead of animating smoothly?

Almost always a missing @property registration. Without telling the browser a custom property holds a <number> (or another interpolatable type), it's treated as an opaque string and can only snap between values — see demo #7 for the exact registration needed.

Is it better to animate box-shadow/filter or use a transform-based alternative?

Transforms are cheaper to render, but not every effect has a transform-based equivalent — a genuine blur or a growing shadow needs filter or box-shadow respectively. Use transforms whenever the effect allows it, and reach for these heavier properties deliberately, on a reasonable number of elements at once.

What's the difference between clip-path and a plain width/height reveal?

A width or height tween changes the element's actual box size, which reflows surrounding layout as it grows. clip-path (demo #6) keeps the element's real size fixed the entire time and only changes what's visible — no layout shift, and it runs on the compositor rather than triggering layout on every frame.

Every demo above ships its full HTML, CSS and JS in the HTML / CSS / JS tabs in its own top bar. A fast way to make this stick: click Fork & Edit on demo #6 and change the four-point polygon to a diagonal wipe instead of a left-to-right one, or fork demo #7 and swap --pct's target from 82 to something driven by a real value in your own project. Save whichever version you like to My Code and it's yours to keep, tweak further, or drop straight into a real project.

Recommended next step: the GSAP Playground covers tweens, timelines, stagger, and ScrollTrigger in the same live-editor format as these demos, free and click-based across 55 lessons. For more tutorials like this one, browse the JavaScript label on the blog.

About the author

Puneet Sharma
Puneet Sharma is a freelance web developer and the creator of FWD Tools and WebDevPuneet. Follow him on X/Twitter

Post a Comment