Skip to content
Zumkai

Performant motion with Claude Code: 60fps, reduced-motion, and Core Web Vitals (with the aino.agency case)

Performant motion with Claude Code: transform-and-opacity rule, tiered reduced-motion, Lenis with INP control, MCP audits, and the aino.agency case.

  • motion performance 60fps accessibility
  • 60fps transform opacity
Dark navy cover with the title Animation performance: 60fps without jank in condensed type, a violet-to-pink gradient rule, and concentric rings on the right.
Contents
  1. Why does motion drop 60fps and hurt Core Web Vitals?
  2. How do I isolate transform and opacity with Claude Code?
  3. How do I honor prefers-reduced-motion in tiers?
  4. How do I tune Lenis without hurting INP?
  5. What does the aino.agency case prove about little JS?
  6. How do I audit motion with Playwright MCP, Chrome DevTools MCP, and Context7?
  7. When do I cut animation instead of optimizing?
  8. Frequently asked questions
  9. Conclusion: rules, reduce branches, and audits before publishing
  10. Sources

Motion breaks publishing at three predictable points. Scroll loses fluidity because the wrong property recomputes layout every frame. Pages shift because pins and reveals enter with no reserved space. Part of the audience receives animation they asked never to see, because no static alternative covers prefers-reduced-motion.

This guide treats all three as acceptance criteria, with a property rule, a tiered reduce branch, Lenis tuned with INP under control, and in-browser audits with Claude Code-ready commands and prompts. For the library map plus the per-job decision rule, return to the complete motion guide.

Why does motion drop 60fps and hurt Core Web Vitals?

Code with transform animation in an editor during a frame measurement
Photo: Negative Space via StockSnap (CC0).

Wrong properties cost frames, layout, and response. Transform and opacity pass through compositing, recomputing no boxes and repainting no full layers. Top, left, width, height, and margin trigger reflow, which recomputes position and size and sinks frame rates on long lists and pinned scroll. That is the standing rule of the 60fps-animation skill in the iart-ai repo, per the iart-ai repo.

The Core Web Vitals tie runs direct. LCP worsens when an animated hero delays the largest visible content. CLS worsens when pins enter with no reserved space or reveals swap heights with no placeholder. INP worsens when inertial smooth, heavy listeners, and long animations delay click and touch answers. The lib analysis with a React and Next.js table, tying Lenis, lerp, and INP together, sits in snigdha from Jun 18, 2026.

The fix starts before code, with three limits. Short durations for gesture confirmation. Reserved space for every pin and reveal. No essential content trapped in motion with no alternative. Those limits turn into an auditable checklist in the audit section, verified through in-browser LCP, CLS, and INP.

What to measure before optimizing

Measure LCP on the hero, CLS on pins and reveals, INP on animated buttons and filters. If LCP climbs with intros, CLS climbs with pins, or INP climbs with smooth scroll, the trouble sits in the motion architecture, never in easing micro-tuning.

How do I isolate transform and opacity with Claude Code?

Skills set the standard, prompts lock the run. The 60fps-animation skill in the iart-ai set teaches the 60fps pattern with transform and opacity, with reflow-free hero, transition, and micro-interaction examples, per the iart-ai repo. Without that lock, the model blends top with translate and width with scale, creating two sources of truth for one move.

Prompts work better with an explicit ban. Describe the component, the allowed property, the banned property, centralized duration and easing. One direct hero example: "Build this hero animation using only transform and opacity, no top, left, width or height animation." Then add selectors, translate and scale values, mobile behavior. For React micro-interactions, the complement is transform layout, as the layout docs show on motion.dev.

js
// Allowed: compositing without reflow
gsap.to(".card", { x: 24, scale: 1.04, opacity: 1, duration: 0.28, ease: "power2.out" });

// Banned in this guide: recomputes layout
// gsap.to(".card", { left: 24, width: 320 });

Centralize duration and easing tokens in one file. Short durations for hover and press, medium for section entrances, no long timelines for click confirmations. Once Claude Code receives ready tokens, it stops inventing curves per section and motion gains steady rhythm.

How do I honor prefers-reduced-motion in tiers?

Laptop on a desk during a reduced-motion branch test
Photo: Negative Space via StockSnap (CC0).

Every meaningful animation needs a legible static equivalent. The legal and technical base is WCAG 2.3.3, covering interaction-triggered animation and demanding a switch to kill nonessential movement. The accessible-animation skill in the same iart-ai repo organizes that into a tiered model, with reduction levels per severity instead of all-or-nothing, per the iart-ai repo.

The reach number explains why that branch is no detail. Past 50% of mobile sites already honored prefers-reduced-motion in 2024, in a Web Almanac reading cited through annnimate. At that adoption level, publishing with no reduce branch means publishing outside the standard half the mobile web already follows.

With GSAP, the split uses gsap.matchMedia. One branch serves no-preference with full pin, scrub, and parallax. The other serves reduce with visible content, no pin or scrub.

js
import gsap from "gsap";

const mm = gsap.matchMedia();

mm.add("(prefers-reduced-motion: no-preference)", () => {
  // full pin, scrub, and parallax here
});

mm.add("(prefers-reduced-motion: reduce)", () => {
  gsap.set(".pinned-hero, .reveal, .layer-back", { clearProps: "all", opacity: 1, y: 0 });
});

With Motion in React, the split uses MotionConfig with reducedMotion="user" plus the useReducedMotion hook to fork variants. The pattern sits in the docs on motion.dev and follows the same matchMedia logic, with a static version losing no information.

jsx
import { MotionConfig, motion, useReducedMotion } from "motion/react";

export function RevealCard() {
  const reduce = useReducedMotion();
  return (
    <MotionConfig reducedMotion="user">
      <motion.div
        initial={{ opacity: 0, y: reduce ? 0 : 24 }}
        whileInView={{ opacity: 1, y: 0 }}
        viewport={{ once: true }}
      />
    </MotionConfig>
  );
}
Mobile sites honoring reduced-motion Horizontal bar chart with over 50 percent of mobile sites honoring reduced-motion, a Web Almanac 2024 reading via annnimate. Reduced-motion on mobile, 2024 base Web Almanac 2024 reading cited via annnimate Mobile sites honoring reduce 50%+ Single bar with cited data. No desktop estimate. Read: honoring reduce went mobile-standard. A reduce branch is no option, it is half the ship. Source: Web Almanac 2024 via annnimate.
Source: Web Almanac 2024 via annnimate, link under Sources.

The tiered model closes the loop. Tier one removes parallax and scrub. Tier two removes autoplay and loops. Tier three freezes transitions into hard cuts with full content. Every tier holds hierarchy and reading, never hiding prices, CTAs, or form feedback.

How do I tune Lenis without hurting INP?

Screen with code during a Lenis lerp tune
Photo: Marc Chouinard via StockSnap (CC0).

Lenis lends scroll inertia, and unbounded inertia delays answers. The lerp parameter drives smoothing, with a cited range between 0.05 and 0.15: low values scroll looser and answer slower, high values scroll tighter and answer faster. The analysis tying that range to INP in React and Next.js stacks sits in snigdha from Jun 18, 2026, with the lenis.on(scroll, ScrollTrigger.update) sync pattern also in youngju.dev from May 16, 2026 and adamarant from May 28, 2026.

For reduced-motion users, the exit is stopping the smooth. The lenis.stop call freezes inertia and returns native scroll, which preserves reading and answers for the reduce branch. Pair with matchMedia to switch Lenis on and off alongside pin and scrub.

js
import Lenis from "lenis";
import { ScrollTrigger } from "gsap/ScrollTrigger";

const lenis = new Lenis({ lerp: 0.1 });
lenis.on("scroll", ScrollTrigger.update);

if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
  lenis.stop();
}

For the full pin, scrub, and parallax base on that bridge, with raf on the GSAP ticker and zeroed lagSmoothing, revisit cinematic scroll with GSAP and Lenis. The working INP rule tests the range with real clicks on filters and menus: if low values delay confirmations, raise lerp or kill smooth on that route and measure again.

What does the aino.agency case prove about little JS?

Aino.agency shows restraint with direction builds identity. The site, SOTD in the Awwwards animation picks, uses ASCII as texture plus plain physics in vanilla, at 30kb of total JS. No animation framework loads the page. Movement answers cursor and scroll in a few lines, and reading follows clear hierarchy.

The useful pattern sits in the economy. One texture technique, ASCII here, plus one short physics pass for gesture answers. No background video, no competing loops, no pins over dense text. Each section requests one gesture and returns one visible change, which holds attention at no bundle cost.

The Claude Code lesson is scope, not tooling. Ask for cheap texture plus short vanilla physics wherever identity allows. Save GSAP, Motion, and vector runtimes for narrative scroll, stateful micro-interactions, and input-driven vectors. When little JS suffices, little JS is the right stack.

How do I audit motion with Playwright MCP, Chrome DevTools MCP, and Context7?

Desk with a laptop during a Core Web Vitals audit
Photo: Negative Space via StockSnap (CC0).

In-browser audits decide what stays live. The flow runs Playwright MCP for navigation and measuring, Chrome DevTools MCP for performance, Context7 for current docs before fixing APIs. That trio's organization, with frontend-design, skill collections, and Figma MCP for handoff, sits in the frontend toolkit for Claude Code.

Playwright MCP runs with automatic vision plus DevTools capacity for LCP, CLS, and INP, through the @playwright/cli package. Chrome DevTools MCP adds performance timelines and traces. Context7 verifies current GSAP, Motion, and Lenis signatures before long correction passes.

bash
npx -y @playwright/cli install --with-deps
npx -y @playwright/cli test --vision auto --caps=devtools

Audit prompts work better literal, with output per file and line. Two prompts cover the loop:

"Audit this page for transform-only animations, CLS from pins and reveals, INP from smooth scroll and long transitions, and prefers-reduced-motion coverage. Report file, line, failing metric and fix."

"Generate a reduced-motion variant for every animated section with static equivalents, no pin, no scrub and no autoplay, following WCAG 2.3.3. Keep headings, CTAs and form feedback fully visible."

The table below compresses the checklist. Each row carries a visible symptom, likely cause, direct fix, and in-browser verification. It works as Definition of Done before deploy.

SymptomCauseFixVerification
Jank on lists and scrollTop, left, width, or height animation with reflowSwap to transform and opacity with short durationsReflow-free timeline in Chrome DevTools MCP, jump-free scroll in Playwright MCP
CLS on pins and revealsPins with no reserved space, reveals with no placeholderReserve pin heights plus reveal placeholdersZeroed CLS on reload with --caps=devtools plus mobile-viewport tests
High INP on clicks and filtersLow lerp with long inertia plus long transitionsRaise lerp inside 0.05 to 0.15 or kill smooth on the routeINP inside bounds on real-click tests through @playwright/cli
Animation with no alternativeNo reduce branch for prefers-reduced-motionAdd matchMedia with reduce plus MotionConfig reducedMotion="user"Reduce-on tests show full static content with lenis.stop applied
Autoplay below the foldPlayers and loops live with no IntersectionObserverPause outside the viewport, play only on entryNo live canvas below the fold mid-scroll in traces
Loops distracting from formsContinuous movement near inputs and CTAsRemove loops from form zones, hold focus visibleForms complete undistracted with feedback intact

When do I cut animation instead of optimizing?

Some animations deserve no fine-tuning. Long intros delaying LCP, parallax fighting paragraphs, loops beside forms, transitions hiding click confirmations make that list. The criterion is chore-based: if motion delays reading, confuses state, or spends attention explaining no change, the fix is cutting and remeasuring reading.

The same bar applies to interactive vectors. Gesture-less loops, page-wide racing Rive, hierarchy-free triggers turn into noise and sink reading. For the inputs call, with Rive for states and dotLottie for light loops, revisit Rive vs Lottie with Claude Code. If the vector shifts no state on gestures, swap autoplay for play-on-entry or drop the instance and freeze the opening frame.

Cutting also simplifies audits. Fewer timelines to measure, fewer reduce branches to hold, less JS to load. On over-budget pages, cutting with motive beats optimizing without criteria.

Frequently asked questions

Why do only transform and opacity hold 60fps?

Because they composite with no box recalculation. Top, left, width, and margin trigger reflow and repaint, which costs frames on scroll and lists. The 60fps-animation skill in the iart-ai repo locks that pattern with reflow-free hero and micro-interaction examples.

How do I switch Lenis and scrub off for reduced-motion users?

Use gsap.matchMedia with a reduce branch, call lenis.stop, and ship static content at opacity 1 with no pin. In React, add MotionConfig reducedMotion="user" with useReducedMotion to fork variants, per motion.dev. The Lenis-with-reduced-motion pattern sits in adamarant from May 28, 2026.

How do I audit LCP, CLS, and INP with Playwright MCP?

Run the @playwright/cli package with --vision auto and --caps=devtools to capture LCP, CLS, and INP mid-navigation, and add Chrome DevTools MCP for timelines. The flow with those MCPs plus Context7 for docs sits in the frontend toolkit for Claude Code.

When does cutting motion beat optimizing?

When it delays chores, confuses state, or fights reading while explaining no change. Long intros over LCP, parallax over text, and loops near forms are direct candidates. Cut, remeasure LCP, CLS, and reading, and reintroduce only with motive plus a reduce branch.

Conclusion: rules, reduce branches, and audits before publishing

Performant motion wants three hits in sequence. The transform-and-opacity rule holding 60fps with no reflow. Tiered reduce branches with matchMedia, MotionConfig, and lenis.stop, in a tiered model tied to WCAG 2.3.3 and 50%+ mobile-site adoption. Lenis with lerp between 0.05 and 0.15 plus INP measured on real clicks. The aino.agency case, SOTD with ASCII and vanilla physics at 30kb of total JS, shows restraint with direction sustains awards. Audits with Playwright MCP, Chrome DevTools MCP, and Context7 close the loop with literal commands and prompts.

The next step follows the bottleneck. For scroll with pin and parallax, return to the GSAP-with-Lenis satellite. For stateful vectors, advance to Rive versus Lottie. For the full map, return to the pillar and settle what stays live.

Sources