Why Only Animate Transform and Opacity for Performance: The Complete Guide
You should only animate transform and opacity because these are the only CSS properties that run entirely on the GPU, bypassing the browser's expensive layout and paint phases to maintain smooth 60fps motion.
Animating the wrong CSS properties can cripple your application's frame rate and introduce visible jank. According to the emilkowalski/skills repository architecture guidelines, restricting animations to transform and opacity is the only reliable path to achieving hardware-accelerated motion without blocking the main thread or forcing costly style recalculations.
Understanding the Browser Rendering Pipeline
Modern browsers render web pages through a multi-step pipeline: layout (calculating geometry), paint (filling pixels), and composite (drawing layers to the screen). Most CSS properties force the browser to execute all three steps on every animation frame. However, transform and opacity are unique compositor-only properties that skip directly to the final composite step on the GPU.
When you animate layout-affecting properties like width, height, margin, or position coordinates, the browser must recalculate the entire page geometry, repaint the affected regions, and then composite. This triple-phase execution saturates the main thread and drops frames under heavy load.
Why Transform and Opacity Are GPU-Only
The emilkowalski/skills repository explicitly documents this performance boundary in skills/review-animations/STANDARDS.md:
"Only animate
transformandopacity— they skip layout/paint and run on the GPU.padding/margin/height/width/top/lefttrigger all three rendering steps."
Transform Properties
The transform property manipulates an element's position, scale, rotation, or 3D orientation entirely within the compositor. Because these geometric changes don't affect the document flow or neighboring elements, the browser can batch these updates on the GPU without consulting the main thread's layout engine.
Opacity Changes
Like transform, opacity adjustments occur during the composite phase. The browser simply adjusts the alpha channel of an existing layer rather than recalculating pixel values across the render tree. This makes opacity transitions extremely cheap even on complex DOM structures.
The Cost of Layout-Triggering Properties
Animating properties that affect layout forces the browser into a performance bottleneck known as layout thrashing. The following table illustrates the rendering cost difference:
| Property | Rendering Phase | Performance Impact |
|---|---|---|
transform |
Composite only | Fast – GPU-only, minimal main-thread work |
opacity |
Composite only | Fast – GPU-only, no re-layout |
width, height, top, left |
Layout + Paint + Composite | Slow – forces complete render tree recalculation |
When animating height or width, the browser must answer geometry questions like "What size is this element now?" and "Where do surrounding elements move?" on every frame. This synchronous layout calculation blocks JavaScript execution and destroys animation smoothness.
Repository Standards and Guidelines
The skills/review-animations/STANDARDS.md file codifies this rule at lines 112-113, establishing it as a hard constraint for all animations in the codebase. This standard ensures that animations remain interruptible and resilient to page load conditions.
Additional constraints appear throughout the repository:
skills/review-animations/SKILL.mdsummarizes the rule in checklist format for quick reference during development.skills/improve-animations/AUDIT.mddemonstrates how to audit existing codebases for violations of the transform/opacity rule.skills/emil-design-eng/SKILL.mdprovides concrete before-and-after examples of property migrations.
Practical Code Examples
The repository provides specific implementation patterns for both vanilla CSS and React animation libraries.
Correct: GPU-Only CSS Animation
Animate transform and opacity to fade and slide elements without layout impact:
.item {
opacity: 0;
transform: translateY(8px);
animation: fadeIn 300ms ease-out forwards;
}
@keyframes fadeIn {
to {
opacity: 1;
transform: translateY(0);
}
}
Incorrect: Layout-Triggering Animation
Never animate dimensional properties that force reflow:
/* Bad: animating height forces layout + paint on every frame */
.box {
height: 0;
animation: grow 400ms ease-out forwards;
}
@keyframes grow {
to {
height: 200px; /* Triggers layout recalculation */
}
}
Framer Motion Implementation
When using Framer Motion, always use the full transform string rather than shorthand properties:
import { motion } from "framer-motion";
// ✅ Correct: Uses compositor thread
<motion.div animate={{ transform: "translateX(100px)" }} />
// ❌ Incorrect: Runs on main thread via requestAnimationFrame
<motion.div animate={{ x: 100 }} />
The shorthands x, y, and scale execute via JavaScript on the main thread, while the complete transform string leverages the browser's native compositor as documented in skills/review-animations/STANDARDS.md (lines 118-122).
Advanced Performance Considerations
Beyond property selection, the repository identifies two additional anti-patterns that negate GPU acceleration.
Avoid CSS Variable Animation on Parents
Updating CSS custom properties on parent elements forces style recalculation for all descendants, effectively bypassing the GPU optimization. The standards file warns against this pattern at lines 113-117, noting that animating a variable like --x via @property still triggers layout if it affects geometry.
Framer Motion Shorthand Limitations
As noted in the source analysis, Framer Motion's shorthand properties (x, y, scale) bypass the compositor and run through requestAnimationFrame. For high-performance scenarios, always expand these to their transform string equivalents to ensure the animation stays on the GPU.
Summary
- Only animate
transformandopacityto ensure GPU-only compositing and avoid layout/paint costs. - Layout properties like
width,height,margin, andpositioncoordinates force expensive recalculation of page geometry on every frame. - Use full transform strings in animation libraries like Framer Motion rather than shorthand properties to maintain hardware acceleration.
- Never animate CSS variables on parent elements, as this forces style recalculation across descendant trees and negates GPU benefits.
Frequently Asked Questions
What happens if I animate width or height instead of transform?
Animating width or height forces the browser to execute the complete rendering pipeline—layout, paint, and composite—on every animation frame. This recalculates the position and size of the animated element and potentially its siblings, blocking the main thread and causing visible stutter or "jank" during the animation.
Why are Framer Motion shorthands like x and y slower than transform strings?
Framer Motion shorthands (x, y, scale) are implemented via JavaScript's requestAnimationFrame running on the main thread, while the full transform CSS string is handled natively by the browser's compositor on the GPU. Under heavy load or concurrent animations, the main-thread approach drops frames, whereas the compositor maintains smooth motion.
Can I use will-change to accelerate other properties?
While will-change can hint to the browser to prepare layers for animation, it does not eliminate the layout and paint costs for properties like width or top. The emilkowalski/skills repository standards explicitly reject this optimization for layout properties, insisting that only transform and opacity provide guaranteed GPU-only rendering paths.
How do CSS variables affect animation performance?
Animating CSS custom properties (variables) on parent elements forces the browser to recalculate styles for all descendants, effectively negating the GPU acceleration benefits. Even if the variable controls a transform value, updating the variable itself triggers style invalidation across the subtree, moving work back to the main thread.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →