Table of Contents
Introduction to CSS Animations
CSS animations allow you to animate HTML elements without JavaScript — using only CSS properties and keyframes. In 2025, CSS animations are more powerful than ever, with scroll-driven animations, view transitions, and native discrete animation support arriving in all modern browsers.
Whether you are a beginner writing your first :hover effect or an experienced developer building complex scroll-driven narrative experiences, this CSS animation tutorial covers everything you need to know. We will start with the fundamentals and progressively build up to the most advanced animation patterns used in production today.
There are two primary ways to create animations in CSS:
- CSS Transitions — Simple animations that go from a starting state to an ending state, triggered by state changes (hover, focus, class toggle).
- CSS Keyframe Animations — Complex multi-step animations with full control over intermediate states, timing, and repetition.
Let us explore both, starting at the beginning.
CSS Transitions: The Foundation
CSS transitions are the simplest form of animation. They smoothly transition a property from one value to another when a state change occurs:
.btn {
background: #6366f1;
color: #fff;
padding: 0.75rem 1.5rem;
border-radius: 8px;
transition: all 0.3s ease;
}
.btn:hover {
background: #4f46e5;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(99, 102, 241, 0.3);
}
Transition Properties
| Property | Description | Example |
|---|---|---|
transition-property | Which property to animate | opacity, transform, all |
transition-duration | How long the animation lasts | 0.3s, 300ms |
transition-timing-function | The acceleration curve | ease, ease-in-out, cubic-bezier() |
transition-delay | Delay before the animation starts | 0.1s, 1s |
Transition Best Practices
- Use
transformandopacityfor performance — these are the only properties the browser can animate without triggering layout recalculations. - Avoid
transition: all— it is convenient but can cause unintended animations and performance issues. Be explicit about which properties you want to transition. - Set appropriate durations — 0.2s-0.3s for micro-interactions, 0.5s-0.8s for larger transitions, and 1s+ for emphasis animations.
CSS Keyframe Animations
When you need more control than a simple transition, keyframe animations let you define multiple steps along an animation timeline:
@keyframes fadeInUp {
0% {
opacity: 0;
transform: translateY(30px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
.animate-in {
animation: fadeInUp 0.6s ease forwards;
}
The animation Shorthand
.element {
animation: name duration timing-function delay iteration-count direction fill-mode play-state;
}
.element {
animation: fadeInUp 0.6s ease 0.2s 1 normal forwards running;
}
/* Or broken out for clarity */
.element {
animation-name: fadeInUp;
animation-duration: 0.6s;
animation-timing-function: ease;
animation-delay: 0.2s;
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
animation-play-state: running;
}
Keyframe Animation Properties
| Property | Options | Description |
|---|---|---|
animation-name | Any @keyframes name | Which keyframe set to use |
animation-duration | 0.5s, 2s, etc. | Time for one complete cycle |
animation-timing-function | ease, linear, cubic-bezier() | Acceleration curve |
animation-delay | 0s, 1s, -0.5s | Delay before start (negative values start mid-animation) |
animation-iteration-count | 1, 3, infinite | How many times to repeat |
animation-direction | normal, reverse, alternate, alternate-reverse | Direction of each cycle |
animation-fill-mode | none, forwards, backwards, both | Styles applied before/after animation |
animation-play-state | running, paused | Whether the animation is active |
Multi-Step Keyframes
@keyframes pulse-scale {
0% {
transform: scale(1);
opacity: 1;
}
25% {
transform: scale(1.05);
opacity: 0.8;
}
50% {
transform: scale(1);
opacity: 1;
}
75% {
transform: scale(1.03);
opacity: 0.9;
}
100% {
transform: scale(1);
opacity: 1;
}
}
.pulse {
animation: pulse-scale 2s ease-in-out infinite;
}
CSS Transform Properties
The transform property is the most performant way to animate elements because it does not trigger layout or paint — only compositing. The common transform functions are:
.element {
/* 2D Transformations */
transform: translateX(100px); /* Move horizontally */
transform: translateY(50px); /* Move vertically */
transform: translate(100px, 50px); /* Move both */
transform: scale(1.5); /* Scale (1.5x) */
transform: scaleX(1.2); /* Scale horizontally */
transform: scaleY(0.8); /* Scale vertically */
transform: rotate(45deg); /* Rotate clockwise */
transform: skewX(10deg); /* Skew horizontally */
/* 3D Transformations */
transform: perspective(800px) rotateY(45deg);
transform: translateZ(50px);
transform: rotate3d(1, 1, 0, 45deg);
/* Multiple transforms */
transform: translateY(-20px) scale(1.05) rotate(-2deg);
}
The transform-origin Property
Controls the point around which transformations are applied:
.card {
transform-origin: center center; /* Default */
}
.card:hover {
transform-origin: top left;
transform: scale(1.1);
}
Animation Timing Functions
Timing functions control how the animation accelerates and decelerates. CSS provides several built-in options, and you can create custom ones with cubic-bezier():
.built-in {
transition-timing-function: ease; /* Default — slow start, fast middle, slow end */
transition-timing-function: linear; /* Constant speed */
transition-timing-function: ease-in; /* Slow start, fast end */
transition-timing-function: ease-out; /* Fast start, slow end */
transition-timing-function: ease-in-out; /* Slow start and end, fast middle */
}
.custom {
transition-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1); /* Custom curve */
}
/* Spring-like effect (overshoots before settling) */
.spring {
transition-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
}
Creating Natural Motion
The secret to great animation is understanding easing. In the real world, nothing moves at a constant speed. Here are production-tested cubic-bezier values for different effects:
| Effect | cubic-bezier Value | Feeling |
|---|---|---|
| Material-style entrance | 0.4, 0, 0.2, 1 | Professional, standard |
| Bouncy entrance | 0.68, -0.55, 0.27, 1.55 | Playful, energetic |
| Snap to position | 0.175, 0.885, 0.32, 1.275 | Precise, satisfying |
| Subtle reveal | 0.23, 1, 0.32, 1 | Smooth, natural |
| Sharp exit | 0.55, 0, 1, 0.45 | Urgent, dismissive |
Practical Animation Examples
Example 1: Staggered Card Entrance
@keyframes cardEntrance {
from {
opacity: 0;
transform: scale(0.9) translateY(20px);
}
to {
opacity: 1;
transform: scale(1) translateY(0);
}
}
.card:nth-child(1) { animation: cardEntrance 0.4s ease-out 0s both; }
.card:nth-child(2) { animation: cardEntrance 0.4s ease-out 0.1s both; }
.card:nth-child(3) { animation: cardEntrance 0.4s ease-out 0.2s both; }
.card:nth-child(4) { animation: cardEntrance 0.4s ease-out 0.3s both; }
.card:nth-child(5) { animation: cardEntrance 0.4s ease-out 0.4s both; }
.card:nth-child(6) { animation: cardEntrance 0.4s ease-out 0.5s both; }
Example 2: Loading Spinner
@keyframes spin {
to { transform: rotate(360deg); }
}
.spinner {
width: 32px;
height: 32px;
border: 3px solid rgba(99, 102, 241, 0.2);
border-top-color: #6366f1;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
Example 3: Shimmer Skeleton Loader
@keyframes shimmer {
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
.skeleton {
background: linear-gradient(
90deg,
var(--bg-card) 25%,
rgba(255, 255, 255, 0.1) 50%,
var(--bg-card) 75%
);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: 8px;
height: 20px;
margin-bottom: 0.5rem;
}
.skeleton--avatar {
width: 48px;
height: 48px;
border-radius: 50%;
}
.skeleton--text-wide {
width: 80%;
}
Example 4: Smooth Page Transitions
@keyframes pageEnter {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes pageExit {
from {
opacity: 1;
transform: scale(1);
}
to {
opacity: 0;
transform: scale(0.98);
}
}
.page-enter {
animation: pageEnter 0.3s ease-out both;
}
.page-exit {
animation: pageExit 0.2s ease-in both;
}
Scroll-Driven Animations
Scroll-driven animations are the most exciting CSS feature for 2025. They allow animations to be driven by the user's scroll position — without any JavaScript. This is part of the CSS Scroll-Driven Animations specification and is supported in Chrome 115+ and with polyfills for other browsers.
Basic Scroll Timeline
@keyframes progress {
from { width: 0%; }
to { width: 100%; }
}
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: #6366f1;
animation: progress linear;
animation-timeline: scroll();
}
This creates a reading progress bar that fills as the user scrolls down the page — entirely in CSS, zero JavaScript.
View Timeline
Animate elements when they enter the viewport:
@keyframes reveal {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.reveal {
animation: reveal 0.6s ease-out both;
animation-timeline: view();
animation-range: entry 0% entry 100%;
}
Each element animates in when it scrolls into the viewport, and the animation is synchronized with its position. No Intersection Observer API needed.
Range Controls
.element {
animation: fadeIn 0.5s linear both;
animation-timeline: view();
animation-range: contain 0% contain 100%;
/* Animation plays while the element is fully inside the viewport */
}
/* Named ranges: cover, contain, entry, exit */
@keyframes parallax {
from { transform: translateY(0); }
to { transform: translateY(-100px); }
}
.parallax-layer {
animation: parallax linear;
animation-timeline: scroll();
}
Performance Optimization
Poorly implemented animations cause jank — visible stuttering that ruins the user experience. Follow these CSS animation tutorial performance guidelines:
The Compositor-Friendly Properties
Only transform, opacity, and filter can be animated entirely on the GPU compositor thread without triggering layout or paint.
/* GOOD — compositor only */
.element {
transform: translateX(100px);
opacity: 0.5;
}
/* BAD — triggers layout */
.element {
width: 200px;
margin-left: 100px;
top: 50px;
}
will-change Hint
Use will-change to inform the browser that an element will be animated, allowing it to optimize ahead of time:
.animated-element {
will-change: transform, opacity;
}
Use will-change sparingly — too many elements can exhaust GPU memory and actually degrade performance.
Hardware Acceleration
Force GPU compositing for complex animations by promoting elements to their own layer:
.heavy-animation {
transform: translateZ(0); /* Old trick — still works */
will-change: transform; /* Modern, explicit approach */
}
Debouncing Animations on Scroll
When using scroll-linked animations via JavaScript, throttle the scroll handler. When using CSS scroll-driven animations, this is handled automatically by the browser — another reason to prefer them.
Accessibility and Animation
Animations can cause discomfort, dizziness, and nausea for users with vestibular disorders. Follow these accessibility best practices:
Respect prefers-reduced-motion
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
This universal selector approach effectively disables all animations and transitions for users who prefer reduced motion. It is the recommended pattern from the W3C.
Motion Budget
Even for users who do not prefer reduced motion, follow a motion budget:
- UI micro-interactions (button hovers, focus states): always allowed
- Entrance animations (cards, sections): 0.3-0.6s, subtle, no scaling from very small sizes
- Emphasis animations (hero text reveal): use sparingly, 0.6-1s
- Background animations (parallax, particle effects): opt-in only or use
prefers-reduced-motion
Flashing Content
Avoid animations that flash more than 3 times per second. Content that flashes faster can trigger seizures in photosensitive epilepsy. The WCAG 2.1 standard forbids flashing content that exceeds 3 flashes per second.
Advanced Techniques
Pausing Animations
Control animations with CSS or JavaScript:
.card {
animation: float 3s ease-in-out infinite;
}
.card:hover {
animation-play-state: paused;
}
Chaining Animations with animation-delay
Create sophisticated sequences by offsetting each animation's start time:
@keyframes flyIn {
from { transform: translateX(-100%) scale(0.5); opacity: 0; }
to { transform: translateX(0) scale(1); opacity: 1; }
}
.letter:nth-child(1) { animation: flyIn 0.5s ease-out 0s both; }
.letter:nth-child(2) { animation: flyIn 0.5s ease-out 0.1s both; }
.letter:nth-child(3) { animation: flyIn 0.5s ease-out 0.2s both; }
.letter:nth-child(4) { animation: flyIn 0.5s ease-out 0.3s both; }
/* Creates a typewriter-like staggered reveal */
Steps() Timing Function
Create frame-by-frame animations with steps():
.sprite-sheet {
width: 100px;
height: 100px;
background: url('sprite.png');
background-size: 600px 100px; /* 6 frames in a row */
animation: play-sprite 1s steps(6) infinite;
}
@keyframes play-sprite {
from { background-position: 0 0; }
to { background-position: -600px 0; }
}
Custom Properties in Animations
Combine CSS custom properties with animations for dynamic theming:
:root {
--animation-scale: 1.05;
--animation-duration: 0.3s;
}
.card {
transition: transform var(--animation-duration) ease;
}
.card:hover {
transform: scale(var(--animation-scale));
}
/* Override per component or per theme */
.prominent-card {
--animation-scale: 1.1;
--animation-duration: 0.5s;
}
FAQ
What is the difference between CSS transitions and animations?
Transitions are simple state-to-state animations triggered by state changes (like hover). Keyframe animations are more complex, supporting multiple steps, repetition, and automatic playback without state triggers.
Which CSS properties are best for animation performance?
transform (translate, scale, rotate) and opacity are the most performant. They can be animated on the GPU compositor thread without triggering layout or paint.
Can I animate height from 0 to auto?
Not directly with CSS transitions. You cannot transition to or from auto. Common workarounds include using max-height instead of height, or using grid-template-rows: 0fr to 1fr (which works in modern browsers).
What are scroll-driven animations?
Scroll-driven animations progress based on the user's scroll position instead of time. The animation-timeline: scroll() and animation-timeline: view() CSS properties enable this without JavaScript. They are supported in Chrome 115+ and available via polyfill for other browsers.
How do I make an animation loop infinitely?
Use animation-iteration-count: infinite. Combine with animation-direction: alternate for a back-and-forth looping animation instead of a resetting one.
How do I handle prefers-reduced-motion?
Use a CSS media query: @media (prefers-reduced-motion: reduce) { /* disable animations */ }. The recommended approach is to set animation-duration: 0.01ms and transition-duration: 0.01ms on all elements, effectively disabling motion while preserving the final state.
Conclusion
CSS animations have evolved from simple hover effects to a complete animation system that can handle transitions, keyframe animations, and even scroll-driven timelines — all without JavaScript. This CSS animation tutorial has covered everything from your first transition to advanced scroll-driven animations used in award-winning web experiences.
The key takeaways:
- Start with transitions for simple state changes — button hovers, link underlines, focus states.
- Use keyframe animations for multi-step sequences, loading states, and attention-grabbing effects.
- Prefer
transformandopacityfor the best performance. - Master
cubic-bezier()to create natural, characterful motion. - Use scroll-driven animations for viewport-triggered reveals without JavaScript.
- Always respect
prefers-reduced-motionfor accessibility. - Test animations on real devices — emulators miss performance issues.
If you want to see these animation techniques in action within a complete design system, the PixelGlass UI Kit includes 50+ animated components with carefully crafted transitions, micro-interactions, and scroll-driven reveals — all built with the principles covered in this CSS animation tutorial. Every component respects prefers-reduced-motion and follows performance best practices.
Related articles: Glassmorphism UI Design Guide | Responsive Web Design Best Practices 2025