| name | web-motion |
| description | Closes the agentic coding loop for web animation. Lets Claude scroll the user's page while recording it (Playwright), extract frames, build a labelled contact sheet, and reason about timing/easing/trajectory frame-by-frame โ plus motion design principles (Disney's 12, adapted for web, GSAP, CSS, scroll input) to name and fix what's wrong. Use whenever the user wants to add, improve, debug, or review any animation โ scroll effects, transitions, hover states, entrance/exit, loading states, micro-interactions. Trigger even if the user doesn't say "animation" but describes something moving, appearing, disappearing, or feeling "off" or "robotic" or "too fast/slow". |
Web Motion
This skill closes the agentic coding loop for web animation. Without it, you write animation code blind โ you can't watch what you just produced, so you rely on the user to describe what feels wrong. With it you get both halves of the loop:
- Vision โ bundled scripts record the page with Playwright (or ffmpeg for hover/click flows), extract frames at 25fps, and build a labelled contact sheet. You read those images directly and reason about timing, easing, and trajectory the way a motion designer reviews a take.
- Judgment โ Disney's 12 Principles of Animation, adapted for the constraints of the web (scroll input, GSAP timelines, CSS transitions, browser rendering). Once you can see what's broken, the principles name what's wrong and point at the fix.
The combination is what matters. Without vision, you guess about an animation you can't perceive. Without the principles, "it feels off" stays unactionable. Together: watch the take โ name the violated principle โ write the fix โ re-record to verify.
Works with the Official GSAP Skills
This skill handles vision and judgment โ recording the page, reading frames, naming what's wrong. The official GSAP skills handle API correctness โ writing GSAP code the right way (timelines, ScrollTrigger, plugins, React, performance).
Together they close the full loop:
- Write โ use the GSAP skills to produce correct animation code
- Record โ this skill records and extracts frames
- Diagnose โ this skill names the violated motion principle
- Fix โ back to GSAP skills to edit the code
- Verify โ re-record and confirm
When fixing animation code mid-session, reach for the appropriate GSAP skill:
| Task | GSAP skill |
|---|
| Tweens, easing, stagger | gsap-core |
| Timeline sequencing, position parameter | gsap-timeline |
| ScrollTrigger, pinning, scrub | gsap-scrolltrigger |
| SplitText, ScrollSmoother, Flip, plugins | gsap-plugins |
| React / Next.js | gsap-react |
| Performance, jank, will-change | gsap-performance |
| Vue, Svelte | gsap-frameworks |
Install: npx skills add https://github.com/greensock/gsap-skills
Or in Claude Code: /plugin marketplace add greensock/gsap-skills
The Core Problem: Linear Input, Non-Linear Perception
The underlying idea: real objects have mass, so they never start or stop instantaneously. They accelerate from rest and decelerate back to rest. A power2.inOut curve is literally a mathematical approximation of that physical behavior.
What makes this especially important for scroll-scrubbed animations is the input problem:
- Scroll is a linear input โ the user drags through progress at roughly constant speed
- Human perception of movement is non-linear โ we judge naturalness by acceleration curves, not constant velocity
So the easing curve acts as a transfer function between the linear scroll input and the non-linear animation output. Strong eases (power3, power4) compensate harder for the mechanical nature of scroll โ the object stays still longer, then commits fast. The user's hand moves at a constant rate but the thing on screen feels like it has weight and momentum.
Studios known for this pattern (Resn, Active Theory, Locomotive) often layer it further:
- A smooth scroll library (Lenis) adds momentum to the scroll itself
- GSAP scrub + strong ease adds momentum to the element within the scroll
- The double-layering makes everything feel heavy and deliberate
The short version: you're using the ease curve to fake physics on a fundamentally physics-free input device. It's why power2.inOut looks natural and a raw scrub: true with no ease looks mechanical โ even though both move the same start and end positions.
Trade-off โ smooth scroll libraries: The double-layering approach (Lenis + GSAP ease) produces a heavier, more deliberate feel that some products want. The downside is added input latency that can read as laggy on fast machines or for users who prefer snappy, native-feeling scroll. Both approaches are valid โ choose based on the product's personality. If in doubt, ask the user which they prefer before reaching for Lenis.
The 12 Principles, Web-Adapted
1. Slow In / Slow Out
Real objects have mass. They accelerate from rest and decelerate back to rest โ they never start or stop instantaneously.
Web: Use ease-in-out curves (power2.inOut, cubic-bezier(0.4, 0, 0.2, 1)) for most movements. For scroll-scrubbed animations, this principle is critical โ it's the transfer function that makes linear scroll feel physical. For outgoing animations that exit the viewport, inOut still works because the deceleration happens off-screen.
2. Anticipation
A small preparatory motion in the opposite direction before the main action signals what's about to happen and adds energy.
Web: Button presses scale down slightly before triggering. A card tilts slightly before flying off. A drawer handle jiggles before the drawer opens. Keep anticipation subtle (0.05โ0.1 scale, 3โ8deg rotation) โ it should register subconsciously, not literally.
3. Follow Through & Overlapping Action
Parts of a system continue moving after the main body stops. Actions in a group start and end at slightly different times rather than all moving together.
Web: Stagger. If multiple elements animate, offset their start times (stagger: 0.08 in GSAP). Elements shouldn't all arrive at exactly the same frame. A reversed stagger (stagger: -0.12) makes the rightmost/last element lead, which often looks more natural for elements entering from below or left.
4. Squash & Stretch
Objects deform to show mass and flexibility โ they compress on impact, elongate when moving fast.
Web: scale(0.95) on button press (squash). Spring eases that overshoot target and bounce back (stretch). scaleY compression on a bouncing loader. Avoid for UI components that must feel rigid/professional โ use for playful, expressive moments only.
5. Staging
Clear presentation of the idea. Composition and timing direct the user's eye to what matters.
Web: Entrance animations should guide reading order, not fight it. Animate the headline first, body text second, CTA last. Use overflow: hidden on containers to create reveal effects without layout shift. Don't animate 6 things at once โ stage them so attention lands where you want it.
6. Secondary Action
Additional actions that support and enrich the main action without competing with it.
Web: Icon rotates while its parent card slides in. Text fades while its container translates. A checkmark draws itself after a form submits. Secondary actions should feel like natural consequences of the primary action โ if they draw attention away from the main event, they're too prominent.
7. Timing
Duration determines perceived weight, speed, and mood.
Web defaults:
100โ150ms โ immediate feedback (button press, focus ring, hover state)
200โ300ms โ UI transitions (dropdown open, tooltip appear, tab switch)
400โ600ms โ meaningful transitions (page section change, modal enter)
600ms+ โ emphasis or storytelling (hero entrance, scroll-driven reveals)
Never animate the same property twice in overlapping durations โ the browser (and the eye) can't track it.
8. Exaggeration
Push the action beyond realism to clarify intent and add personality.
Web: Spring eases that overshoot by 10โ15% before settling. Rotation angles on a scatter effect pushed to ยฑ60โ70deg instead of ยฑ20deg. Scale on hover pushed to 1.08 instead of 1.02. Exaggeration should feel playful, not broken โ calibrate to the personality of the product.
9. Arc
Most natural movements follow curved paths rather than straight lines. Straight-line motion feels mechanical and robotic.
Web: When an element moves diagonally, animate x and y with slightly different eases so the path curves. Cards "thrown" off screen should follow arcs, not straight vectors. In GSAP, motionPath plugin handles literal arcs; for simple cases, offset ease timings create the illusion.
10. Depth (from Solid Drawing)
Objects exist in 3D space with weight, volume, and shadow. In flat UI, depth is simulated.
Web: Parallax (foreground moves faster than background). perspective + rotateX/Y for card tilt on hover. Shadows that shift as elements lift (box-shadow scaling on hover). z-index layering that's reinforced by scale โ elements "above" others scale slightly larger on entrance.
11. Pose to Pose vs. Straight Ahead
Two approaches: define key states and interpolate between them (pose to pose), or simulate physics frame by frame (straight ahead).
Web: CSS transitions and GSAP tweens are pose to pose โ define start and end, let the browser interpolate. Physics engines (Matter.js, Cannon.js) or spring simulations are straight ahead. Most UI animation is pose to pose. Use straight ahead only when the motion is too complex or unpredictable to keyframe โ falling debris, cloth, fluid.
12. Appeal (Motion Language Consistency)
The overall animation system should feel coherent and have personality. Every motion choice communicates something about the product.
Web: Establish a consistent easing vocabulary and stick to it. If entrance animations use power2.inOut, exit animations should too โ they're the same object, same physics. Don't mix spring eases with linear eases on the same component. Fast, snappy motion says confident and modern. Slow, heavy motion says thoughtful and premium. Both are valid โ be intentional and consistent.
Animation Safety Rules
Before writing any animation code, check these โ they're easy to miss and cause broken layouts:
Horizontal overflow โ GSAP from animations with x transforms, or any element wider than the viewport, will create a horizontal scrollbar. Always add to the page root:
html { overflow-x: clip; }
clip (not hidden) is preferred โ it doesn't create a new stacking context and doesn't interfere with position: fixed elements.
box-sizing โ If box-sizing: border-box is not set globally, padding adds to max-width, making containers wider than intended. Always confirm the project has:
*, *::before, *::after { box-sizing: border-box; }
GSAP from initial state โ gsap.from() immediately applies the start state. If that start state is off-screen (e.g., y: '100%', x: '-100%'), the element is invisible before ScrollTrigger fires. Use gsap.fromTo() or set immediateRender: false if this causes layout flash.
Percentage transforms vs pixel values โ x: '100%' in GSAP means 100% of the element's own width, not the viewport. For an element 800px wide, that's 800px off to the right โ well outside the viewport. Use pixel values when you need precise control.
Scroll-Scrubbed Animation Patterns
When building scroll-driven animations (GSAP ScrollTrigger or CSS scroll-timeline):
Structure:
- Incoming phase โ element enters with strong ease (Principle 1). Duration ~25% of scroll range.
- Dwell โ element holds still. Leave a gap in the timeline (no tweens = nothing moves). Duration ~40โ50% of scroll range.
- Outgoing phase โ element exits with matching ease (mirror of incoming). Duration ~25% of scroll range.
Key rules:
- Use the same ease family for incoming and outgoing โ they're the same object with the same physics
- Size the section height to give each phase enough scroll distance to feel intentional
scrub: true (no number) ties 1:1 to scroll. scrub: 1 adds 1s lag โ use only when that lag reads as deliberate weight, not as latency
Incoming stagger: reversed (stagger: -0.12) so elements arrive in the natural reading order (left/top first).
Outgoing stagger: forward (stagger: 0.07) so the first element leads the exit.
Animation Debugging: Frame-by-Frame Inspection
When an animation feels wrong but it's hard to articulate why (too fast, wrong timing, disappears too quickly, feels abrupt), the most reliable method is to capture a video and inspect every frame as an image. This lets you find the exact frame where things go wrong and measure visible durations precisely.
Always ask the user before starting a recording. Something like: "Want me to record the animation so I can inspect it frame by frame?" โ never start recording silently.
First-time setup
Before recording, check that the skill's dependencies are installed:
bash ~/.claude/skills/web-motion/scripts/doctor.sh
If anything is missing, run setup once (it asks consent for system installs):
bash ~/.claude/skills/web-motion/scripts/setup.sh
This installs ffmpeg (via Homebrew on macOS, apt on Linux), the playwright npm package inside the skill directory, and the Chromium browser (~300MB). A .installed marker is written on success so future runs skip the check.
Step 1: Record and extract โ one command
For scroll-driven animations (the common case), run:
bash ~/.claude/skills/web-motion/scripts/analyze.sh http://localhost:PORT/your-page.html
This records the page with Playwright auto-scrolling, converts to mp4, and extracts frames at 25fps. Output lands in /tmp/web-motion-<timestamp>/frames/.
Flags:
--duration <s> โ minimum total recording time from page load. The default window is only ~2.5s (1s settle + scroll + 0.5s tail); for page-load animations, always pass a duration comfortably longer than the full animation, e.g. analyze.sh <url> 0 1 --duration 6.
--start <s> / --end <s> โ trim the frame-extraction window, so dead time before/after the animation doesn't dilute the contact sheet.
--fps <n> โ extraction framerate (default 25). Raise to 50 to inspect fast easings frame-by-frame.
For pure page-load animations where the first ~1s matters (analyze.sh waits 1s after load before its scroll phase, so the very start of an intro can be missed), record with the autoplay recorder instead โ it captures from t=0 with no scrolling and no pre-wait โ then extract frames manually:
node ~/.claude/skills/web-motion/scripts/record-autoplay.mjs <url> [seconds] [outDir]
bash ~/.claude/skills/web-motion/scripts/extract-frames.sh <video.webm> [fps] [outDir] [start-s] [end-s]
For animations that need real user interaction (hover, click, manual scroll), use the manual ffmpeg scripts instead:
bash ~/.claude/skills/web-motion/scripts/record-ffmpeg-macos.sh
bash ~/.claude/skills/web-motion/scripts/extract-frames.sh output.mp4
Step 2: Map the timeline with a contact sheet
Before reading individual frames, build a contact sheet โ a 6ร4 grid of evenly-sampled frames, each labelled with its source frame number:
bash ~/.claude/skills/web-motion/scripts/contact-sheet.sh /tmp/web-motion-*/frames
Read the resulting contact-sheet.png once. You'll see the entire animation timeline at a glance โ entrance, dwell, and exit phases are immediately visible, and the frame-number labels tell you exactly where to drill in.
Step 3: Drill into the interesting window
Once the contact sheet has told you roughly when each phase happens, read individual frames within those windows:
- Use the contact sheet to identify candidate frame numbers (e.g., "entrance starts around f60, dwell at f80โf115, exit around f120")
- Read every 2โ3 frames within those windows using the Read tool
- Note the exact frame numbers where the animation starts, peaks, and ends
- Calculate visible duration:
(end_frame - start_frame) / fps = seconds visible
Look for:
- Animation starting or ending too abruptly (ease not applied or too weak)
- Elements visible for only a handful of frames (wrong end position, exiting immediately)
- Unexpected jumps between frames (competing tweens, wrong label positioning)
- Outgoing animations that don't mirror the incoming feel
- Containers clipping elements during scatter/exit (
overflow: hidden on a parent)
Fix the issue in code, re-run analyze.sh, regenerate the contact sheet, and repeat until the frame inspection confirms the desired behavior.
Quick Reference: Ease Selection
| Situation | Ease |
|---|
| Element entering viewport | power2.inOut or power3.inOut |
| Element exiting viewport | power2.inOut (deceleration happens off-screen) |
| Button / immediate feedback | power2.out (fast start, settles) |
| Object being thrown / launched | power3.in (accelerates into exit) |
| Object landing / settling | power3.out (decelerates into rest) |
| Playful bounce / elastic | elastic.out(1, 0.5) or back.out(1.7) |
| Mechanical / deliberate | power1.inOut |