Animations, Transitions & Motion
Every jank frame is a broken promise to the user. Senior engineers don't just make things move â they understand which properties are cheap to animate, why some animations drop frames, and how to measure the difference.
The Rendering Pipeline
Understanding animation performance requires understanding what the browser does for each frame:
JavaScript â Style â Layout â Paint â Composite
â â â â
recalc styles reflow repaint GPU compositing
Cheapest animations only trigger: âââââââââââââââââââ Composite
Mid-cost animations trigger: âââââââââ Paint â Composite
Expensive animations trigger: â Layout â Paint â CompositeProperty Animation Cost Tiers
| Tier | Properties | Pipeline Cost | FPS Impact |
|---|---|---|---|
| Composite-only | transform, opacity | Compositing only | 60fps smooth |
| Paint | color, background, box-shadow, border-radius | Paint + Composite | Usually fine, watch on low-end devices |
| Layout | width, height, margin, padding, top, left, font-size | Layout + Paint + Composite | Janky, avoid for animations |
The golden rule: Animate only transform and opacity. Everything else is a compromise.
CSS Transitions
Transitions animate between two states â triggered by a property change (hover, class toggle, etc.):
.button {
background: var(--color-primary);
transform: scale(1);
transition: transform 200ms ease-out, background 150ms ease;
}
.button:hover {
background: var(--color-primary-dark);
transform: scale(1.05);
}Timing Functions
.element {
/* Keywords */
transition-timing-function: ease; /* Slow start and end */
transition-timing-function: ease-in; /* Slow start */
transition-timing-function: ease-out; /* Slow end (most natural) */
transition-timing-function: ease-in-out; /* Slow start and end */
transition-timing-function: linear; /* Constant speed */
/* Custom cubic-bezier for a snappy feel */
transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1);
/* Step functions for sprite animations */
transition-timing-function: steps(6, jump-none);
}| Use Case | Recommended Easing | Why |
|---|---|---|
| Entering/appearing | ease-out | Decelerates into resting position |
| Exiting/disappearing | ease-in | Accelerates away naturally |
| State change | ease-in-out | Smooth transition between states |
| Micro-interaction | cubic-bezier(0.34, 1.56, 0.64, 1) | Slight overshoot feels responsive |
| Loading spinner | linear | Constant rotation |
Keyframe Animations
For multi-step or looping animations:
@keyframes slide-in {
from {
transform: translateX(-100%);
opacity: 0;
}
60% {
opacity: 1;
}
to {
transform: translateX(0);
}
}
.panel {
animation: slide-in 300ms ease-out forwards;
}
.spinner {
animation: rotate 1s linear infinite;
}
@keyframes rotate {
to { transform: rotate(360deg); }
}animation-fill-mode: forwards is critical: without it, the element snaps back to its pre-animation state when the animation completes.
The FLIP Technique
FLIP (First, Last, Invert, Play) animates layout changes using only transform â turning an expensive layout animation into a cheap composite one:
function flipAnimate(element, changeFunction) {
// First: record initial position
const first = element.getBoundingClientRect();
// Last: apply the DOM change (causes layout)
changeFunction();
const last = element.getBoundingClientRect();
// Invert: calculate the delta and apply inverse transform
const deltaX = first.left - last.left;
const deltaY = first.top - last.top;
const deltaW = first.width / last.width;
const deltaH = first.height / last.height;
element.style.transform = `translate(${deltaX}px, ${deltaY}px) scale(${deltaW}, ${deltaH})`;
element.style.transformOrigin = 'top left';
// Play: remove the transform with an animation
requestAnimationFrame(() => {
element.style.transition = 'transform 300ms ease-out';
element.style.transform = '';
element.addEventListener('transitionend', () => {
element.style.transition = '';
element.style.transformOrigin = '';
}, { once: true });
});
}Why it works: The layout change happens instantly (one forced layout). The animation only uses transform, which is compositor-only. The user sees smooth movement even though the DOM position changed instantly.
will-change: Use and Misuse
will-change tells the browser to promote an element to its own compositor layer ahead of time:
/* Correct: applied just before animation */
.card:hover {
will-change: transform;
}
.card:hover .overlay {
transform: translateY(0);
}/* WRONG: permanent will-change on many elements */
* {
will-change: transform; /* Creates hundreds of compositor layers */
}
.every-card {
will-change: transform, opacity; /* Wastes GPU memory */
}Every will-change layer consumes GPU memory. A page with 200 cards each having will-change: transform can crash mobile browsers. Apply it dynamically, remove it after animation completes.
View Transitions API
The View Transitions API provides animated transitions between DOM states:
document.startViewTransition(() => {
updateDOM();
});/* Customize the transition animation */
::view-transition-old(root) {
animation: fade-out 200ms ease-out;
}
::view-transition-new(root) {
animation: fade-in 300ms ease-in;
}
/* Named transitions for specific elements */
.hero-image {
view-transition-name: hero;
}
::view-transition-old(hero) {
animation: scale-down 300ms ease-in-out;
}
::view-transition-new(hero) {
animation: scale-up 300ms ease-in-out;
}View Transitions work with MPA (multi-page apps) too, via the @view-transition at-rule:
@view-transition {
navigation: auto;
}CSS Scroll-Driven Animations
Scroll-driven animations bind animation progress to scroll position instead of time:
@keyframes reveal {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: reveal linear both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}Timeline Types
/* Scroll progress â tied to scroll container position */
.progress-bar {
animation: grow-width linear;
animation-timeline: scroll(root);
}
@keyframes grow-width {
from { transform: scaleX(0); }
to { transform: scaleX(1); }
}
/* View progress â tied to element entering/leaving viewport */
.section {
animation: fade-in linear both;
animation-timeline: view();
animation-range: entry 10% cover 40%;
}These replace JavaScript scroll listeners with purely declarative, compositor-driven animations.
prefers-reduced-motion
Respecting user motion preferences is not optional:
/* Global reduced-motion override */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
/* Progressive enhancement approach: no motion by default */
.card {
opacity: 1;
}
@media (prefers-reduced-motion: no-preference) {
.card {
animation: fade-in 300ms ease-out;
}
}The progressive approach is superior â animations are opt-in, not opt-out. Users who haven't set a preference get motion; users who have don't.
Web Animations API
WAAPI provides JavaScript control with CSS animation performance:
const animation = element.animate(
[
{ transform: 'translateX(0)', opacity: 1 },
{ transform: 'translateX(100px)', opacity: 0 }
],
{
duration: 300,
easing: 'ease-out',
fill: 'forwards',
}
);
animation.onfinish = () => element.remove();
animation.pause();
animation.reverse();
animation.playbackRate = 2;
const finished = await animation.finished;WAAPI runs on the compositor thread (for transform/opacity), gives programmatic control, and returns Promises â unlike CSS animations which require animationend event listeners.
@starting-style for Entry Animations
@starting-style defines the initial state for elements entering the DOM (or transitioning to display: block):
.dialog {
opacity: 1;
transform: scale(1);
transition: opacity 200ms, transform 200ms, display 200ms allow-discrete;
@starting-style {
opacity: 0;
transform: scale(0.95);
}
}
.dialog[hidden] {
opacity: 0;
transform: scale(0.95);
}This enables CSS-only entry/exit animations without JavaScript or keyframes.
Interview Signal
Senior candidates demonstrate:
- Pipeline awareness â Explaining why
transform/opacityare cheap (compositor-only) whilewidth/toptrigger full layout - FLIP understanding â Describing the technique and why it converts layout animations to transform animations
will-changediscipline â Knowing the memory cost and applying it surgically, not globally- Motion accessibility â
prefers-reduced-motionas a first-class concern, progressive enhancement approach - Modern API awareness â View Transitions, scroll-driven animations, WAAPI as replacements for JS-heavy animation libraries