Under the Hood
Gsap

How GSAP animates anything: the property model and plugins

The interpolation core writes a plain number straight onto target[prop], which is exactly right for a JavaScript object and says nothing at all about a DOM node, an SVG shape, or a color, so this lesson unpacks the getter/setter architecture and the plugin layer that let GSAP treat every one of those as just a bag of numbers to read from and write to.

How GSAP animates anything: the property model and plugins

GSAP's marketing copy likes to say it can animate "anything" — DOM elements, SVG, canvas, WebGL cameras, plain JavaScript objects, even values that don't visually exist anywhere, like a variable you're only using to drive a callback. That's not hyperbole and it's not because GSAP contains a huge amount of special-case code for every one of those targets. It's because the interpolation core from the first lesson never needed to know what a target is in the first place, and everything that looks like "GSAP understands CSS" or "GSAP understands SVG" is a thin translation layer sitting in front of that same tiny core.

Go back to the fifteen-line Tween class and look at the one line that actually touches the target:

this.target[this.prop] = this.start + (this.end - this.start) * progress;

That's a plain property write. target[prop] = number. Nothing about interpolation, easing, or timing cares what target is or what prop means — the entire core only needs two capabilities from a target: a way to read its current value as a number, and a way to write an interpolated number back. Everything else in this lesson is about how GSAP gets those two capabilities for targets that aren't as cooperative as a plain object.

The getter/setter architecture

For a plain JavaScript object, reading and writing a property really is just obj[prop] and obj[prop] = value — the core class from lesson one works on objects unmodified, no translation needed:

const counter = { value: 0 };

gsap.to(counter, {
  value: 100,
  duration: 2,
  onUpdate: () => console.log(Math.round(counter.value)),
});
// counter.value climbs from 0 to 100 over 2 seconds — a genuine tween
// driving nothing but a number sitting in memory.

There is no DOM node here, no visual element at all. gsap.to() built the exact same tween object as always: it read counter.value as the start, 100 as the end, and on every ticker frame it wrote a new interpolated number back into counter.value. This is precisely why GSAP is a natural fit for driving a <canvas> redraw, a WebGL uniform, or a Three.js camera position — none of those are DOM properties, they're just numbers sitting on some object, and "just a number on an object" is the only thing the core has ever required.

Generalize that pattern and you get the real architecture: for every property GSAP is asked to animate, it needs to resolve a getter (how to read the current value as a number) and a setter (how to write an interpolated number back). For a plain object those are trivial — property access. For a DOM element they are not trivial at all, and that's where the core stops being enough on its own.

Why a DOM target breaks the plain-object assumption

Try the same trick on a DOM element with gsap.to(el, { x: 200 }) and immediately hit three problems the plain-object case never had:

  1. There is no el.x property. DOM elements don't expose x — GSAP's x is a convenience name that has to become transform: translateX(...) on the real element.
  2. CSS values carry units. el.style.width isn't a bare number, it's a string like "140px". The core's arithmetic (start + (end - start) * progress) only works on numbers — someone has to strip the unit off before interpolating and put a unit back on before writing.
  3. The current value has to be read from the render tree, not a plain property. To snapshot a "start" value for something like width, you generally need the computed style, not an inline style attribute that may not even be set.

None of that is a flaw in the core — the core was never supposed to solve it. It's a flaw in the assumption "reading and writing a property is always obj[prop]." GSAP's answer is to not touch the core at all and instead insert a translation layer in front of it, for exactly the targets that need one.

CSSPlugin: the translator for DOM targets

CSSPlugin is the piece of GSAP that intercepts a DOM target and supplies real getters and setters in place of the plain-object defaults, so the interpolation core keeps doing exactly what it always did — lerp between two numbers — while CSSPlugin handles everything on either side of that lerp:

  • Unit resolution. For width: 300, CSSPlugin reads the element's current computed width (say "140px"), parses out the 140, uses 300 as the numeric end, runs the ordinary numeric lerp between them, and writes the result back as ${value}px. If you write width: "50%" instead, it resolves against the percentage basis instead. The core still only ever sees two plain numbers and a progress fraction — the unit is stripped before the lerp and reapplied after.
  • Transform shorthands. x, y, rotation, and scale are not real CSS properties at all — they're names CSSPlugin invented because animating the real underlying property, transform, directly is awkward: a single transform string can hold a translate, a rotate, and a scale all at once, and naively overwriting the whole string on every tween would clobber whatever another tween had already set. CSSPlugin maintains its own internal record of each transform component per element and re-serializes the full transform string from all of them together whenever any one changes, so gsap.to(el, {x: 100}) and gsap.to(el, {rotation: 45}) running at the same time compose correctly instead of overwriting each other. The matrix math this implies — how translate/rotate/scale combine into one transform — is its own lesson: Transforms and the matrix.
  • Colors. backgroundColor: "#ff0000" can't be lerped as one number — CSSPlugin parses both the start and end colors into channels (red, green, blue, and alpha), runs the ordinary numeric lerp independently on each channel, and reassembles the result into a color string on write. Progress 0.5 between red and blue isn't some blended CSS keyword, it's the literal midpoint of each RGB channel, recombined.
  • Reading the real start value. Rather than trusting an inline style attribute that might not be set, CSSPlugin reads the element's current computed value the first time the tween renders — which is also precisely the "snapshot lazily, at first render" behavior the first lesson described as a core property of the tween itself. CSSPlugin doesn't override that behavior; it just supplies a smarter getter for the core to call when that snapshot happens.
gsap.to(".card", {
  x: 200,               // CSSPlugin: number -> translateX() inside transform
  rotation: 15,          // CSSPlugin: number -> rotate() inside the same transform
  backgroundColor: "#2e7d32", // CSSPlugin: channel-by-channel color lerp
  width: "60%",          // CSSPlugin: percentage-based unit resolution
  duration: 1.2,
});

Every one of those four properties still bottoms out in the exact same lerp from lesson one. CSSPlugin's entire job is deciding what to read as the numeric start, what to read as the numeric end, and what string to write back once the core hands it an interpolated number — it never reimplements interpolation itself.

Plugins register to claim property names

CSSPlugin isn't hardwired into the core — it's registered the same way any GSAP plugin is, and that registration is what keeps the core tiny. When you call gsap.to(target, vars), GSAP looks at the properties in vars and, for each one, checks whether some registered plugin has claimed that name for this kind of target. A DOM target with a width property routes through CSSPlugin. A property prefixed attr: (as in attr: { r: 40 } for an SVG circle's radius) routes through the plugin that knows how to read and write element attributes rather than styles. A scrollTo property routes through the ScrollToPlugin, which knows how to translate a number into window.scrollTo or an element's scrollTop. morphSVG routes through MorphSVGPlugin, which does something far more involved than a channel-wise lerp — interpolating between two path shapes point by point — but from the core's point of view it's still just handing that plugin a progress value and letting the plugin decide what to write.

Every one of these plugins implements the same two-sided contract the core has always exposed: given a target and a property name, supply a getter and a setter. The core doesn't grow a special case for SVG or scroll or morphing — it just keeps calling render(progress) on whatever plugin claimed the property, and the plugin owns the messy target-specific translation entirely on its own side of that boundary. This is also why third-party and custom plugins are possible at all: registering a plugin is registering a translator, not patching the interpolation engine.

The consequence: GSAP isn't a DOM library with extras bolted on

It's tempting to describe GSAP as "a DOM animation library that also happens to support SVG and canvas." The property model says the opposite is true: GSAP is a number-interpolation engine, full stop, and DOM support is one plugin among several sitting on top of it — an unusually important plugin, since most people only ever use GSAP to animate CSS, but architecturally no different in kind from ScrollToPlugin or MorphSVGPlugin. Strip CSSPlugin out entirely and gsap.to(counter, {value: 100}) still works exactly as shown above, because the core never depended on it. That's the payoff of keeping the core as small as the previous lesson found it: every new kind of target GSAP has ever supported — and every kind some plugin author invents in the future — is a new translator bolted onto an interpolation engine that hasn't had to change shape since lesson one.

Where this goes next

The property model closes the third gap from the original tween core: how a plain number becomes a real, visible change. The fourth gap is still open — how many tweens, each with their own duration and start time, get sequenced and controlled as a single unit. That's Timelines as a data structure, which treats a timeline as, satisfyingly, another kind of tween — one whose "value" is a playhead position instead of a target property.

Go deeper

  • GSAP docs — CSSPlugin The real plugin this lesson describes — unit handling, the transform shorthands, and the full list of CSS-specific behavior it adds on top of the core.
  • GSAP docs — how plugins register The registration mechanism plugins use to claim property names, referenced in this lesson as the reason the core never needs a special case per target type.
  • MDN — CSS transform The single real property that x/y/rotation/scale all serialize into, which lesson six unpacks in full via the transform matrix.

Check yourself

Answer out loud, as if an interviewer asked. If you hand-wave, reread that section.

  1. What are the only two capabilities the interpolation core actually needs from a target, and why does a plain JavaScript object already provide both for free?
  2. Give a concrete example of animating a value that has no visual representation at all, and explain why the core doesn't need to change to support it.
  3. Name the three specific problems a DOM element's width property creates for the plain start + (end - start) * progress formula, and how CSSPlugin solves each one.
  4. Why can't GSAP just overwrite the whole CSS transform string on every tween of x, y, rotation, or scale, and what does CSSPlugin do instead?
  5. Describe, mechanically, how CSSPlugin interpolates between two colors. Is progress 0.5 a CSS concept or a per-channel numeric one?
  6. What does it mean for a plugin to 'register to claim a property name'? Use attr: or scrollTo as your example.
  7. Explain the claim 'GSAP is a number-interpolation engine with a DOM plugin' by describing what would and wouldn't still work if CSSPlugin were removed entirely.