Loading...
Loading...
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".
npx skill4agent add iart-ai/web-animation-skills 60fps-animationwidthheighttopleftmarginpaddingbox-shadowfiltertransformopacityheight: auto| Animating (expensive) | Triggers | Replace with (cheap) |
|---|---|---|
| Layout + Paint | |
| Layout + Paint | |
| Paint | Animate |
| Paint (heavy) | Cross-fade two layers via |
| Paint | |
| 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); }scaleXbox-shadow::afteropacity.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; }transformfunction 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));animate()getBoundingClientRect()height: auto/* 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; }interpolate-size: allow-keywordscalc-size(auto, size)offsetWidthgetBoundingClientRectscrollTopgetComputedStyle// 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)
});
// GOOD: batch all reads, then all writes
const widths = items.map((el) => el.offsetWidth); // all reads
items.forEach((el, i) => { // all writes
el.style.width = widths[i] * 1.5 + 'px';
});requestAnimationFramewill-change: transform.menu { will-change: transform; } /* only on elements about to animate */will-change: autotransform: translateZ(0)will-changePackaged helper ():scripts/freezes thescripts/seek-shot.sh anim.html 0 1.5 3harness and screenshots each moment;?t=Ntiles them for one-glance review. Seescripts/contact-sheet.sh sheet.png frame-*.png.scripts/README.md
.html.html@keyframes@keyframes?t=Nel.style.animationDelay = (-N)+'s'; el.style.animationPlayState = 'paused'anim.pause(); anim.currentTime = N*1000?t=0scaleXnpx playwright screenshottracingLayoutPaintnpx playwright screenshot --wait-for-timeout=500 "file://$PWD/anim.html?t=0.3" frame.pngtransformopacityscaleXwill-changeprefers-reduced-motion| Goal | Do this |
|---|---|
| Move element | |
| Resize without distortion | FLIP with |
| Shadow on hover | Animate |
| Expand to content height | grid |
| Many elements moving | Batch |
| Smooth animation start | |
| Verify it's compositor-only | DevTools Performance: no purple "Layout"/green "Paint" per frame |
scaleX/scaleYtransformleft: %will-changetranslateZ(0)filterbackdrop-filter1fr→0froverflow: hiddenmin-height: 0@media (prefers-reduced-motion: reduce)references/patterns-and-profiling.md