Spurlock Studios
Contact
Share LinkedIn X
A small stack of coins. Thesis: MOTION SYSTEMS SHIP GSAP SCROLL.

Award reels lie. They compress a site into a silent 1080p capture on a plugged-in laptop. Production tells the truth: mid-range Androids, hotel wifi, Safari address-bar jitter, and users who never asked for your scrubbed timeline. A motion system that ships is a numbered budget — transform and opacity first, teardown on every trigger, and prefers-reduced-motion as a designed cut — so cinematic sites still feel intentional after they leave Figma and meet Core Web Vitals. This spoke sits under Websites That Feel Like Films.

The short answer

  • Critical copy and the LCP node stay visible if JavaScript is late, blocked, or dead.
  • Motion stays on transform and opacity unless someone writes an exception and owns the jank.
  • Every ScrollTrigger, listener, and will-change loan has a teardown path.
  • prefers-reduced-motion: reduce is a first-class composition, not animation: none sprinkled in QA.
  • One earned pin or scrub per page. Unlimited motion is how Lighthouse dies.

If a stakeholder cannot accept those five lines, you do not have a motion problem. You have a scope problem.

What is a motion system that actually ships?

A motion system is the contract, tokens, recipes, and bans that keep GSAP from becoming a second design system nobody documented. It is not a folder of timelines. It is the rule that says which properties may move, how long they may move, who tears them down, and what the page looks like when the user asked the OS to reduce motion.

I treat it like type and color: shared easing families, shared duration steps, and a written kill list. Random easings per section make the brand feel accidental. Consistency reads as craft even when individual moves are simple.

LayerWhat it decidesWho signs it
ContractVisible without JS; transform-only default; teardown; reduced-motion cut; numbered capsFounder + lead engineer
TokensTwo easings, three durations, stagger stepsDesign + engineering
RecipesHero entrance, section reveal, one scrub, control hoverEngineering
BansLayout properties, full-bleed blur, ambient loops, extra pinsThe kickoff doc

Hundreds of production sites have not made me chase Awwwards captures. They have made me chase the page that still reads on a warm phone after a hard refresh.

Why do award reels lie about production motion?

They lie because the capture is a controlled experiment and the visitor is not. The reel is desktop Chrome, charged battery, muted audio, a mouse that never hits a focus ring, and a GPU that is not sharing the bus with Slack. The visitor is Safari on iOS with the URL bar showing and hiding, Chrome on a mid-range Android that thermal-throttles after twenty seconds of blur, and a reduced-motion setting the designer never toggled.

Reel conditionProduction conditionWhat breaks
Plugged-in laptopMid-range phone on 4GMain-thread jank, dropped frames
Silent 1080p captureReal audio policy + data capsAutoplay video, surprise sound
Mouse-only pathKeyboard, VoiceOver, back gestureFocus traps, trapped horizontal scroll
no-preference assumedOS reduce-motion onInvisible headlines, unfinished layout
One continuous takeRoute changes, back/forward cacheDuplicate ScrollTriggers

The reel is a sales artifact. It is not a QA environment. If the only proof of the motion pass is a screen recording, the pass is not done.

What belongs in a transform-only budget?

Chrome’s own performance writing is blunt: keep animations on compositor-friendly properties. How to create high-performance CSS animations and Animations and performance both say to stick to transform and opacity so you skip layout and paint. Stick to compositor-only properties names the same pair and warns that extra layers cost memory on the devices you are trying to protect.

That is the budget. Not a vibe. A property allowlist.

PropertyCostDefault
x / y / translateCompositorAllowed
scale, rotateCompositorAllowed
opacityCompositorAllowed
clip-path on a small regionPaint-ish; test on low-endException, written
SVG stroke draw, sane path countMixedException, written
width, height, top, leftLayoutRefused
Continuous filter: blur() on large layersPaint + GPU heatRefused
Box-shadow pulses on many nodesPaintRefused
Scroll-linked blur/brightness on a full-bleed heroPaint + LCP riskRefused

Cheap and preferred:

  • x, y, scale, rotate, opacity
  • clip-path carefully, on small regions, tested on low-end devices
  • SVG stroke draws when the path count is sane

Expensive and usually refused:

  • animating width, height, top, left
  • continuous filter: blur() on large layers
  • box-shadow pulses on many elements
  • scroll-linked blur/brightness on full-bleed heroes

If someone wants a layout-property tween, they write the exception in the kickoff table and they own the mid-Android test. Unsigned exceptions do not ship.

How do you keep GSAP from leaking triggers?

GSAP is still the right tool when you need timeline precision, scroll scrubbing, and cleanup that does not leave zombie pins. The cleanup API is not optional. gsap.context() exists to record every tween, timeline, and ScrollTrigger created inside a function so you can revert() them in one call. On marketing sites I wrap work in a context so React and Astro islands can tear down on unmount.

useEffect(() => {
  const ctx = gsap.context(() => {
    gsap.from(".hero-line", {
      y: 24,
      opacity: 0,
      stagger: 0.08,
      duration: 0.7,
      ease: "power3.out",
    });
  }, rootRef);
  return () => ctx.revert();
}, []);

In React, GSAP’s own React guide prefers useGSAP() over a raw useEffect because Strict Mode double-mounts will otherwise spawn duplicate ScrollTriggers. Either path is fine if revert() actually runs. A context you never revert is a leak with extra syntax.

SituationCleanupFailure if you skip it
Astro / React island unmountctx.revert() or useGSAP teardownDuplicate pins, stacked listeners
Route change in an SPA shellRevert the page contextOld triggers fire on the new page
Breakpoint crossgsap.matchMedia() auto-reverts the branchDesktop pin lives on a 390px viewport
Reduce-motion toggle mid-sessiongsap.matchMediaRefresh()Motion keeps running after the OS flip

gsap.matchMedia() is a specialized context. Do not nest a second gsap.context() inside it. Call mm.revert() when the island dies.

When is a pin earned?

ScrollTrigger will pin anything you ask. That is the problem. Pinned sections are cinematic and costly. They fight iOS rubber-banding, the collapsing Safari chrome, sticky headers, and nested scrollers. GSAP’s scroll overview sells pin + scrub as the product. On a marketing homepage, pin + scrub is a rationed scene, not a default.

Cap homepage pins. Prefer a short pin that teaches one idea over a multi-scene epic that traps a thumb. Always test rubber-banding and address-bar show/hide on iOS — scroll math that is perfect in desktop Chrome often jitters on phones.

When a pin is mandatory, keep the pinned content light.

Pin choiceWhen it is earnedKill it when
Short pin, one idea, light DOMThe scene is the offer or the proofMid-Android jank, header collision
Scrubbed timeline, one per pageThe story needs scroll authorshipA second scrub appears “for craft”
Horizontal pinned trackDesktop-only, keyboard equivalent existsMobile, back-gesture traps, no focus path
Multi-scene pin stackAlmost never on a brochure homepageAny phone test, any Lighthouse mobile drop

Pinned-content rules:

  • Fewer images
  • No autoplay video
  • No nested scrollers
  • No sticky header fighting the same axis
  • pinSpacing understood — flex parents silently disable the padding you expected

If you cannot name the one idea the pin teaches, it is decoration. Decoration does not get a pin.

iOS is where pins go to die. The visual viewport shrinks when the URL bar shows, then grows when it hides. ScrollTrigger measured against window.innerHeight at refresh time will be wrong the next time the chrome moves. Procedure I run on every earned pin:

  1. Refresh on iPhone Safari with the URL bar visible, then with it hidden.
  2. Rubber-band past the pin start and past the pin end.
  3. Rotate to landscape and back. Call ScrollTrigger.refresh() if the layout actually changed.
  4. Confirm the sticky site header does not share the pinned axis.
  5. Confirm the back gesture can leave a horizontal track.

If step 2 or 3 jitters, shorten the pin or delete it. Do not add a smooth-scroll library to “fix” Safari. That adds a second clock.

How should reduced-motion be authored?

Author the static composition first. Then add motion for no-preference. That order is the whole method. MDN’s prefers-reduced-motion feature exists because scaling and panning large objects are vestibular triggers. WCAG 2.2 Success Criterion 2.3.3 Animation from Interactions (AAA) says motion animation triggered by interaction can be disabled unless it is essential. Sufficient techniques are C39 in CSS and SCR40 in JavaScript.

Reduced-motion is a first-class cut. It is not a late animation: none !important.

/* Static composition is the default. */
.hero-line { opacity: 1; transform: none; }

@media (prefers-reduced-motion: no-preference) {
  .hero-line { /* motion tokens only live here */ }
}

In GSAP, branch with matchMedia so the reduce path sets a finished state instead of leaving opacity: 0 waiting for a tween that will never run:

const mm = gsap.matchMedia();
mm.add("(prefers-reduced-motion: reduce)", () => {
  gsap.set(".hero-line", { clearProps: "all", opacity: 1, y: 0 });
});
mm.add("(prefers-reduced-motion: no-preference)", () => {
  // timelines
});
Reduced-motion jobDo thisDo not do this
HeroInstant, readable end-stateHeadline stuck at opacity: 0
Section revealFull opacity, no riseHalf-applied inline styles
Scrub / pinSkip the pin; keep the contentKeep the pin with duration: 0
Hover / focusColor or underline, no large moveDisable focus styles with the motion
Ambient loopOffSlower loop “as a courtesy”

QA both cuts. If the reduced-motion build looks unfinished, the motion was hiding weak layout.

An in-page “reduce motion” checkbox is not the OS query. Wire it to gsap.matchMediaRefresh() so every matching branch reverts and re-runs, the same way a resize would. A class toggle that only sets animation: none leaves inline GSAP styles on the node. Those leftover translate and opacity values are how “we support reduced motion” still ships a broken hero.

ControlWhat it should doWhat a fake control does
OS prefers-reduced-motionmatchMedia branch + finished gsap.setCSS animation: none over live inline styles
In-page togglematchMediaRefresh() after the flag flipsHides keyframes, leaves opacity: 0
Essential motion (rare)Document why it staysCall a decorative pin “essential” so you can keep it

What should load before the first tween?

Ship the hero in its final visual state in HTML and CSS. Let motion enhance. If the headline starts at opacity: 0 in CSS and only GSAP reveals it, a failed chunk load produces a blank brand. That is not an edge case. That is a launch incident waiting.

Optimize Largest Contentful Paint cares about the actual LCP node painting. A hero that is invisible until a 180kb animation library downloads has already lost, even if the tween is transform-only.

Load stepRuleKill criteria
HTML / CSS heroEnd-state visible without JSText or LCP image hidden in CSS
Fontsfont-display that does not blank the offerInvisible offer for 1.5s of choreography
GSAP chunkAfter first paint, or deferredBlocks LCP
ScrollTriggerOnly on pages that earned a scrubLoaded site-wide “just in case”
VideoPoster first; click-to-play defaultAutoplay + sound + 40MB MP4

will-change is a loan. web.dev’s animation guide and MDN’s will-change both warn against promoting the whole page. Promote the active tween, then release. Blanket will-change: transform creates memory pressure on the exact devices you are trying to protect. GSAP handles a lot of this if you stay inside its APIs. Raw CSS often does not.

.hero-line.is-tweening { will-change: transform, opacity; }
.hero-line.is-settled { will-change: auto; }

When is Framer enough and GSAP required?

Framer is a strong home for component-native motion when the team iterates in design tools and the page count stays in marketing territory. Use it when designers own interaction timing, you need fast marketing iteration, and the site is not fighting for every Lighthouse point under heavy CMS complexity.

Reach for GSAP-in-code when you need scrubbed production timelines, complex cleanup across route changes, or a static shell (Astro and friends) with islands. Webflow interactions cover a wide middle. Escalate when the interaction graph outgrows the panel UI.

ToolUse it whenLeave it when
CSS transitionsHovers, focus, simple entersScrubbed scenes, sequenced pins
FramerDesigners own timing; marketing pagesYou need production revert across routes
Webflow interactionsMid-complexity, panel-maintainedThe graph outgrows the panel
GSAP + ScrollTriggerScrubbing and timeline authorship are the productThe maintainer cannot read the context
WebGL / shadersRare, budgeted, phone-testedPhones stutter; first on the cut list

Teams ask for “Awwwards motion” with a brochure budget and a Webflow-only maintainer. Be explicit in the kickoff. The tool is a maintenance decision, not a taste decision.

How do motion and Lighthouse collide?

They collide when the timeline hides the LCP node, when pins add layout work, and when the motion pass is scored on desktop after the visual lock. Pair this spoke with Lighthouse 90+ Without Killing the Design when scores collapse after the motion pass. Do not delete the brand to buy two points. Delete the unbudgeted scene.

CollisionWhat you seeWhat you do
Hero hidden until GSAPBlank fold, late LCPEnd-state in HTML; enhance after paint
Layout-property tweenCLS + jankRewrite to transform / opacity
Extra pinsTBT / INP proxies climbCut to one earned scene
Full-bleed blur on scrollThermal throttle after 20sKill the filter; keep the still
Desktop-only LighthouseGreen circle, angry phonesRe-run mobile after the motion pass

Motion that delays comprehension also hurts conversion. If the primary CTA arrives 1.5 seconds after a choreographed logo draw, you traded bookings for a portfolio clip. Sequence meaning first: brand and offer readable immediately, then presence animations that do not rearrange the reading order.

Main-thread work during scroll is the other collision. A scrub that reads getBoundingClientRect on many nodes every tick will show up as INP-adjacent jank even when every property is a transform. Keep ScrollTrigger callbacks thin. Do not run image decode, video play, or layout reads inside onUpdate. If the scene needs a side effect, fire it once on onEnter / onLeave, not sixty times a second.

How do you sequence a marketing narrative?

Think in scenes, not in “can we animate this div.” A homepage might be five scenes: arrival, offer, proof, work, contact. Each scene gets at most one signature move. Shared section reveals can repeat. Signature moves should not. When every block has a unique choreography, the brand feels busy rather than directed.

I storyboard motion in a table during kickoff. If a row is empty because nobody can name the move, that scene stays static.

SceneSignature moveFallback (reduced motion)
ArrivalLine rise + soft dim on plateStatic composition
OfferAccent underline drawInstant underline
ProofStaggered quote opacityFull opacity list
WorkHorizontal drag or simple gridGrid, no drag
ContactFocus on the formForm visible

Shared easing, shared duration families:

TokenValueUsed for
Ease Apower3.outEnters, reveals
Ease Bpower2.inOutScrubs, directional moves
Duration S0.2–0.3sControls, focus
Duration M0.5–0.7sSection reveals
Duration L≤1.2s totalHero entrance, one shot

Pick two easings and three duration steps for the whole site. Write them down. The next engineer should not invent a fourth family to “make this section cooler.”

Which GSAP plugins should a marketing site load?

Marketing sites rarely need the full plugin zoo. ScrollTrigger covers most scroll needs. Flip is powerful and easy to misuse. Draggable is great for work boards and dangerous for accessibility if keyboard equivalents are missing. Observer can replace brittle wheel hacks.

Pin versions. A plugin upgrade mid-project that changes default scrub smoothing will burn hours. Keep a short internal note: GSAP version, plugins used, and where contexts live in the codebase.

PluginDefaultLoad it when
ScrollTriggerAllowed, one sceneYou earned a pin or scrub
FlipOffState-to-state layout, phone-tested
DraggableOffKeyboard equivalent ships in the same PR
ObserverOffWheel hacks are already hurting you
SplitTextOffYou accept the extra DOM and the a11y pass
MotionPathOffAlmost never on a brochure homepage

Dependency discipline is part of the motion system. “We might want Flip later” is not a reason to ship it on page one.

How should design hand off motion?

Hand off motion as specs, not vibes. Framer collapses some of that handoff when designers build the motion themselves. On custom stacks, written specs prevent the “make it cooler” loop that never ends.

Spec fieldExampleWhy it exists
Triggerload / inview / scroll progress / hover / clickStops “it just happens”
Propertiesy, opacityEnforces the transform-only budget
Duration + easing tokensM + Ease AStops one-off curves
Stagger0.08sRepeatable, not eyeballed per card
Mobile differenceNo pin below 768matchMedia branch, not hope
Reduced-motion behaviorInstant end-stateFirst-class cut
Teardown ownerIsland revert()Cleanup is a person, not a wish

Review the budget in the same meeting as the mood board. Motion added after visual lock is how scope explodes.

Paste this into a kickoff:

ItemCapKill criteria
Hero entrance≤1.2s totalCompetes with LCP
Section revealShared 0.5–0.7sCauses layout shift
Scrubbed scene0–1 / pageJank on mid Android
Cursor / trail effectsDesktop onlyBreaks focus or battery
Background videoClick-to-play defaultAutoplay + sound risk

What failure modes have already burned production?

These are not theoretical. They are the reasons I write the contract before the first tween.

FailureWhat it costsWhat you do instead
Headline hidden until the animation library downloadsBlank brand, failed LCPEnd-state in HTML/CSS
Pin + sticky header on the same axisJitter, covered CTAOne owner of vertical lock
Lenis / smooth-scroll stacked on ScrollTriggerTwo clocks, one scrollbarOne scroll authority
Horizontal section with no keyboard pathTrapped focus, broken back gestureGrid fallback; desktop-only drag
Undocumented timeline labelsNext engineer afraid to touch productionTokens, recipes, bans in the repo
will-change on every sectionMemory pressure, worse jankLoan, then release
Reduce-motion as a late CSS patchInvisible nodes, unfinished pageStatic cut authored first

Clients often want “cinematic” and arrive with a 40MB hero MP4. Prefer a high-quality still plus subtle transform motion (a ken-burns pan, grain overlay, light leaks as CSS or SVG). Short looping fragments only when file weight is controlled and playback is intentional. Poster-first video with click-to-play for anything with narrative audio or long runtime. A still that feels directed beats a soft autoplay video that heats phones and fails LCP.

Ambient loops also collide with WCAG 2.2.2 Pause, Stop, Hide when the page starts motion on its own. If it moves without a tap, it needs a stop or it needs to be off.

What QA proves the motion pass is done?

The capture video is not the proof. The proof is a device matrix, a reduced-motion cut that still looks designed, and a mobile Lighthouse run after the motion pass — not before.

SurfaceWhat you checkPass
Mid-tier Android, Chrome, throttled 4GScroll the full homepage for a minuteNo late thermal jank
iPhone Safari, notch devicesRubber-band, URL bar show/hide, pinsNo jitter, no covered CTA
Keyboard onlyTab order through animated overlaysNo focus trap
Hard refresh, cold cacheHero readable before GSAPBrand visible
Navigate away and backSPA-like shellsNo duplicate triggers
Lighthouse mobile after motionScores vs the written budgetNo unbudgeted regression
OS reduced-motion onBoth Windows/macOS and iOS pathsFinished static cut

Checklist before merge:

  • Hero text is in the HTML end-state
  • New timelines use only transform / opacity, or a written exception
  • Every gsap.context / matchMedia has revert
  • Reduced-motion path is designed and QA’d
  • Lighthouse mobile did not regress past the agreed budget
  • No second scroll library claiming authority
  • Marketing signed the motion budget table

If any box is unchecked, the pass is not done — even if the capture looks perfect.

Record the phone model, OS reduce-motion state, and Lighthouse mobile URL in the PR. A reel attached to the ticket is optional. The matrix is not.

If the PR only has a desktop capture, send it back. Award reels lie; the matrix is the receipt.

In a Spurlock Studios website sprint, motion is never the first deliverable. Fold and static composition come first. Motion is the pass that adds presence once the conversion job is true. Explore the websites lane when you want that order on a real build. The pillar Websites That Feel Like Films is the authorship frame; this spoke is the budget that keeps the reel from becoming the spec.

FAQ

Is GSAP still worth it for marketing sites in 2026?

Yes when you need precise timelines, scroll control, and reliable cleanup. For lighter marketing motion, CSS or Framer may be enough. Choose by complexity and who maintains the site, not by portfolio fashion. A brochure site with a Webflow-only maintainer does not become a GSAP shop because a reel looked expensive.

How do I keep scroll animations from destroying performance?

Limit pins, stick to transform and opacity, keep pinned DOM light, test on real phones, and tear down triggers on unmount. One earned scrubbed scene beats five decorative ones. If Lighthouse mobile falls after the motion pass, cut a scene before you cut the photography. See Lighthouse 90+ Without Killing the Design for the score tradeoff.

Should the hero animate on load?

Optionally, and never in a way that hides text before JavaScript runs. Ship the final hero state in HTML and CSS, then enhance. If the animation fails, the brand must still be readable. A 1.2s entrance that competes with LCP is not a hero. It is a delay.

How do I handle prefers-reduced-motion?

Design a complete static variant first. Do not leave elements stuck invisible. Use gsap.matchMedia() or the CSS query in C39 so reduced motion is intentional. If the reduce cut looks unfinished, the motion was covering a weak layout.

When should I use Framer instead of hand-coded GSAP?

Use Framer when designers own interaction and the project is marketing-page centric. Use hand-coded GSAP when you need production scrubbing, custom routing cleanup, or a performance-first static architecture. The decision is who maintains the timelines after launch, not which tool won last year’s reel.

What is a sane motion budget for a homepage?

One hero entrance, shared section reveals, at most one scrubbed scene, micro-interactions on controls, and almost no ambient loops. Write the caps down before design starts. Unlimited motion is how mid-range phones and Lighthouse both fail.

CTA

Want a cinematic site that still works on a warm phone, not just in a silent reel?

Explore custom websites or book a sprint at /contact?intent=websites-sprint.

FAQ

What questions does this article answer?

Is GSAP still worth it for marketing sites in 2026?
Yes when you need precise timelines, scroll control, and reliable cleanup. For lighter marketing motion, CSS or [Framer](https://www.framer.com) may be enough. Choose by complexity and who maintains the site, not by portfolio fashion. A brochure site with a Webflow-only maintainer does not become a GSAP shop because a reel looked expensive.
How do I keep scroll animations from destroying performance?
Limit pins, stick to transform and opacity, keep pinned DOM light, test on real phones, and tear down triggers on unmount. One earned scrubbed scene beats five decorative ones. If Lighthouse mobile falls after the motion pass, cut a scene before you cut the photography. See [Lighthouse 90+ Without Killing the Design](/blog/lighthouse-without-killing-design) for the score tradeoff.
Should the hero animate on load?
Optionally, and never in a way that hides text before JavaScript runs. Ship the final hero state in HTML and CSS, then enhance. If the animation fails, the brand must still be readable. A 1.2s entrance that competes with LCP is not a hero. It is a delay.
How do I handle prefers-reduced-motion?
Design a complete static variant first. Do not leave elements stuck invisible. Use [`gsap.matchMedia()`](https://gsap.com/docs/v3/GSAP/gsap.matchMedia()/) or the CSS query in [C39](https://www.w3.org/WAI/WCAG22/Techniques/css/C39) so reduced motion is intentional. If the reduce cut looks unfinished, the motion was covering a weak layout.
When should I use Framer instead of hand-coded GSAP?
Use [Framer](https://www.framer.com) when designers own interaction and the project is marketing-page centric. Use hand-coded GSAP when you need production scrubbing, custom routing cleanup, or a performance-first static architecture. The decision is who maintains the timelines after launch, not which tool won last year's reel.
What is a sane motion budget for a homepage?
One hero entrance, shared section reveals, at most one scrubbed scene, micro-interactions on controls, and almost no ambient loops. Write the caps down before design starts. Unlimited motion is how mid-range phones and Lighthouse both fail.
Sources

Last reviewed

More from this lane

Websites

All →
Start a sprint