accessible-animation
This skill should be used when the user asks to "respect prefers-reduced-motion", "honor reduced motion", "make my animations…
This skill should be used when the user asks to "fix janky CSS animation", "make animation 60fps", "stop layout thrashing", "animate width/height/top/left smoothly", "convert animation to transform", "animate box-shadow performantly", "animate height auto", "FLIP animation", or
$ npx -y skills add iart-ai/web-animation-skills --skill 60fps-animation --agent claude-codeHow it fires
How this skill gets triggered: by you, by Claude, or both.
/60fps-animationContext preview
The summary Claude sees to decide when to auto-load this skill.
This skill should be used when the user asks to "fix janky CSS animation", "make animation 60fps", "stop layout thrashing", "animate width/height/top/left smoothly", "convert animation to transform", "animate box-shadow performantly", "animate height auto", "FLIP animation", or
name: 60fps-animation description: This skill should be used when the user asks to "fix janky CSS animation", "make animation 60fps", "stop layout thrashing", "animate width/height/top/left smoothly", "convert animation to transform", "animate box-shadow performantly", "animate height auto", "FLIP animation", or "why is my scroll/hover animation choppy". version: 0.1.0
Eliminate the #1 cause of janky web animation: animating properties that force the browser to recalculate layout (reflow) or repaint on every frame. The browser renders in stages — **layout → paint → composite**. Animating `width`, `height`, `top`, `left`, `margin`, `padding`, `box-shadow`, or `filter` re-runs layout and/or paint each frame, blocking the main thread. Animating **`transform`** and **`opacity`** runs entirely on the compositor (often the GPU), skipping layout and paint, which is what makes animation smooth at 60/120fps.
Use when an animation stutters or drops frames, when a hover/scroll effect feels heavy, when animating size/position/shadow, when a layout change needs to animate smoothly (cards reordering, an element moving between containers), when animating `height: auto`, or when reviewing animation code for performance.
Map every "expensive" animation to a cheap equivalent.
| Animating (expensive) | Triggers | Replace with (cheap) | |---|---|---| | `width` / `height` | Layout + Paint | `transform: scaleX()/scaleY()` (+ FLIP for true size) | | `top` / `left` / `margin` | Layout + Paint | `transform: translate()` | | `box-shadow` | Paint | Animate `opacity` of a pseudo-element holding the shadow | | `filter: blur()` | Paint (heavy) | Cross-fade two layers via `opacity`, or accept sparingly | | `background-position` | Paint | `transform: translate()` on a child layer | | `color` / `background` | Paint | Often acceptable; or cross-fade layers |
/* BAD: animates layout every frame */
.box { transition: left 300ms, width 300ms; left: 0; width: 100px; }
.box:hover { left: 200px; width: 200px; }
/* GOOD: compositor-only */
.box {
transition: transform 300ms ease;
transform: translateX(0) scaleX(1);
transform-origin: left center;
}
.box:hover { transform: translateX(200px) scaleX(2); }`scaleX` distorts inner content (text stretches). For true resizing without distortion, use **FLIP**.
Animating `box-shadow` repaints a large blurred region every frame. Instead paint the shadow once on a `::after`, then animate only its `opacity`.
.card { position: relative; }
.card::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
box-shadow: 0 12px 28px rgba(0,0,0,0.35);
opacity: 0;
transition: opacity 300ms ease;
pointer-events: none;
}
.card:hover::after { opacity: 1; }The blurred shadow is rasterized once; hovering only changes a compositor opacity — smooth at any frame rate.
FLIP (First, Last, Invert, Play) animates a *layout* change (reorder, resize, move between containers) using only `transform`. Measure where the element was (First) and will be (Last), apply an inverting transform so it visually appears unmoved, then animate the transform back to identity (Play). The DOM ends in its real final layout; the motion is pure compositor work.
function flip(el, mutate) {
const first = el.getBoundingClientRect(); // First
mutate(); // change the DOM/layout
const last = el.getBoundingClientRect(); // Last
const dx = first.left - last.left;
const dy = first.top - last.top;
const sx = first.width / last.width;
const sy = first.height / last.height;
el.animate(
[
{ transformOrigin: 'top left',
transform: `translate(${dx}px, ${dy}px) scale(${sx}, ${sy})` }, // Invert
{ transformOrigin: 'top left', transform: 'none' }, // Play
],
{ duration: 300, easing: 'cubic-bezier(0.2, 0, 0, 1)' }
);
}
// usage: animate an element moving to a new grid position
flip(card, () => targetContainer.appendChild(card));The Web Animations API `animate()` runs the transform on the compositor. For many elements, batch all `getBoundingClientRect()` reads *before* any mutation (see layout thrashing below).
`height: auto` is not interpolatable historically. Modern and fallback approaches:
/* Modern (Chrome 129+/supporting browsers): opt size keywords into animation */
.panel {
interpolate-size: allow-keywords; /* enables animating to/from auto */
height: 0;
overflow: clip;
transition: height 300ms ease;
}
.panel.open { height: auto; }
/* calc-size() also works: height: calc-size(auto, size); *//* Robust fallback today: CSS grid 1fr -> 0fr */
.wrapper {
display: grid;
grid-template-rows: 0fr; /* collapsed */
transition: grid-template-rows 300ms ease;
}
.wrapper.open { grid-template-rows: 1fr; }
.wrapper > .content { overflow: hidden; min-height: 0; }The grid trick animates the track size (compositor-friendly enough and broadly supported) and needs no JS height measurement. Use `interpolate-size: allow-keywords` / `calc-size(auto, size)` where the target browsers support it; keep the grid technique as the cross-browser default.
Reading a layout property (`offsetWidth`, `getBoundingClientRect`, `scrollTop`, `getComputedStyle`) after a write forces a *synchronous* reflow. Interleaving reads and writes in a loop ("layout thrashing") can run dozens of forced reflows per frame.
// BAD: read, write, read, write... forces reflow each iteration
items.forEach((el) => {
const w = el.offsetWidth; // read (forces layout)
el.style.width = w * 1.5 + 'px'; // write (invalidates layout)
});
// GOOWeb animation skills for AI coding agents — GSAP, SVG, Lottie, micro-interactions, page transitions, and 60fps performance, installed straight into your agent.
This skill should be used when the user asks to "respect prefers-reduced-motion", "honor reduced motion", "make my animations…
This skill should be used when the user asks to "make an ASCII animation", "build a terminal/CLI intro or loader", "convert an…
This skill should be used when the user asks to "add a glassmorphism effect", "frosted glass UI", "Apple liquid glass style",…
This skill should be used when the user asks to "build a scroll animation", "add ScrollTrigger", "pin a section while scrolling",…
This skill should be used when the user asks to "add a Lottie animation", "play a .lottie or .json animation", "integrate a…
This skill should be used when the user asks to "add a hover/press effect", "animate a toggle or switch", "build an animated like…