Transitions & Animations
The transition Property
A transition smoothly animates a property when it changes value — usually on :hover or :focus. The syntax is: transition: property duration timing-function delay.
/* Single property */ .btn { transition: background-color 0.3s ease; } /* Multiple properties */ .card { transition: transform 0.25s ease, box-shadow 0.25s ease, opacity 0.2s ease; } /* All properties (use sparingly — can be slow) */ .el { transition: all 0.3s ease; } /* With delay */ .tooltip { transition: opacity 0.2s ease 0.1s; } /* ↑ delay */ /* Practical button example */ .btn { background: #2de8c0; transform: translateY(0); transition: background 0.2s ease, transform 0.15s ease; } .btn:hover { background: #0d9488; transform: translateY(-3px); }
Timing Functions
The timing function controls how speed changes over the duration of a transition or animation. The difference between a cheap-feeling and premium-feeling animation is almost always the timing function.
/* Built-in keywords */ transition-timing-function: ease; /* default — slow, fast, slow */ transition-timing-function: ease-in; /* slow start */ transition-timing-function: ease-out; /* slow end — feels natural */ transition-timing-function: ease-in-out; /* slow both ends */ transition-timing-function: linear; /* constant speed */ /* cubic-bezier() — full control */ transition-timing-function: cubic-bezier(0.34, 1.56, 0.64, 1); /* spring overshoot */ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); /* Material Design */ /* steps() — for sprite animations */ transition-timing-function: steps(8, end);
↑ Hover each card to see the easing in action
The transform Property
transform lets you move, scale, rotate, and skew elements without affecting layout. Unlike changing top or margin, transforms are GPU-accelerated — they're fast and don't trigger layout recalculations.
/* Translate — move */ transform: translateX(20px); /* move right */ transform: translateY(-10px); /* move up */ transform: translate(20px, -10px); /* both axes */ /* Scale */ transform: scale(1.2); /* 20% bigger */ transform: scale(0.8); /* 20% smaller */ transform: scaleX(1.5); /* stretch horizontally */ /* Rotate */ transform: rotate(45deg); /* clockwise 45° */ transform: rotate(-90deg); /* counter-clockwise */ /* Skew */ transform: skewX(15deg); /* Combine — order matters! */ .card:hover { transform: translateY(-8px) scale(1.03) rotate(1deg); } /* 3D transforms */ transform: perspective(500px) rotateY(30deg);
@keyframes Animations
While transitions react to state changes, @keyframes animations run automatically — on load, on loop, or triggered by a class. You define waypoints (0%, 50%, 100%) and the browser fills in the motion between them.
/* 1. Define the keyframes */ @keyframes fadeSlideIn { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } @keyframes pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } /* 2. Apply — shorthand: name duration timing iteration direction fill-mode */ .hero-title { animation: fadeSlideIn 0.6s ease-out both; } .loader { animation: spin 1s linear infinite; } .badge { animation: pulse 2s ease-in-out infinite; } /* Individual properties */ .el { animation-name: fadeSlideIn; animation-duration: 0.6s; animation-timing-function: ease-out; animation-iteration-count: 1; /* or infinite */ animation-direction: normal; /* or reverse, alternate */ animation-fill-mode: both; /* keeps end state */ animation-delay: 0.2s; }
transform and opacity is free — they're handled entirely by the GPU and never trigger layout. Animating width, height, top, margin, or padding forces the browser to recalculate layout on every frame — this causes jank. Always use transform: translate() instead of changing top/left.Stagger & Practical Patterns
Some of the most impressive animation effects are simple CSS techniques applied strategically. Staggering delays, hover card lifts, and loading skeletons are patterns you'll use in every real project.
@keyframes fadeUp { from { opacity: 0; transform: translateY(24px); } to { opacity: 1; transform: translateY(0); } } .card { animation: fadeUp 0.5s ease-out both; } /* Stagger via nth-child delay */ .card:nth-child(1) { animation-delay: 0.0s; } .card:nth-child(2) { animation-delay: 0.1s; } .card:nth-child(3) { animation-delay: 0.2s; } /* Or with CSS custom properties from JS */ .card { animation-delay: calc(var(--i) * 0.1s); }
@keyframes shimmer { from { background-position: -200px 0; } to { background-position: calc(200px + 100%) 0; } } .skeleton { background: linear-gradient(90deg, rgba(255,255,255,.03) 0%, rgba(255,255,255,.08) 50%, rgba(255,255,255,.03) 100%); background-size: 200px 100%; animation: shimmer 1.5s ease-in-out infinite; border-radius: 6px; }
| animation-fill-mode | Behavior |
|---|---|
| none | Element reverts to original styles when animation ends (default) |
| forwards | Element keeps the end-state styles after animation finishes |
| backwards | Apply from-state during delay period |
| both | Combines forwards + backwards — almost always what you want |
animation-fill-mode: both do?transform: translate(-50%, -50%) do (a classic centering trick)?ease-out timing function best suited for?Create a full-viewport hero section with: (1) a title that fades up on load using
@keyframes fadeUp, (2) a subtitle that fades up 0.15s later, (3) a CTA button that fades up at 0.3s, (4) a pulsing gradient background blob, (5) a "scroll down" arrow that bounces with @keyframes bounce. Use animation-fill-mode: both on all entrance animations. Bonus: add a hover effect on the button with a transform: scale(1.05) transition.
💡 Show hints
- Define one
@keyframes fadeUpand reuse it on all three elements with different delays - For the gradient blob: position:absolute, border-radius:50%, background:radial-gradient(...), animation with scale and opacity
- Use
animation-fill-mode: bothso elements are invisible before their delay fires - Bounce arrow:
@keyframes bounce { 0%,100%{transform:translateY(0)} 50%{transform:translateY(8px)} } - Button hover: put transition on the button base, not the :hover rule