Web Animations: From Motion to Meaning
URL: https://ruvebal.github.io/web-atelier-udit/lessons/en/web-animations/
📋 Table of Contents
- ⚡ Quick Start (10 minutes)
- 🎯 Learning Objectives
- 🎭 Critical Perspective: Animation as Language
- 📜 The Evolution of Web Animation
- 🎓 Theory: The 3 Pillars of CSS Animation
- ⚡ Performance: The “Will-Change” Optimization
- ♿ Accessibility: Respecting User Motion Preferences
- 🏗️ Atelier Workshop: Building Animation Systems
- 🎨 Exercise 1: Micro-Interactions (Button States)
- 🎨 Exercise 2: Page Load Animations (Staggered Fade-In)
- 🎨 Exercise 3: Loading States (Skeleton Screens & Spinners)
- 🎨 Exercise 4: SVG Animations (Icons & Logos)
- 🎯 Advanced Practice: Morphing Shapes (CSS clip-path)
- 🎯 Advanced Practice: Scroll-Driven Animations (Modern CSS)
- 🏆 Expert Challenge: View Transitions API
- 📚 Animation Performance Checklist
- 🎓 Atelier Reflection: When NOT to Animate
- 🌍 Real-World Case Studies
- 📝 Assignment: Animate Your Portfolio
- 🎯 Advanced Optional Challenges
- 📚 Further Learning
- 🎓 Atelier Final Reflection
- 📝 Commit Your Learning
⚡ Quick Start (10 minutes)
- Create
styles/animations.cssand paste the snippet below. - Add the HTML exactly as shown to any page.
- Toggle ‘Reduce Motion’ in OS to verify accessibility.
Demo: Open interactive demo
/* Minimal animation system for the workshop */
:root {
--duration-fast: 200ms;
--duration: 300ms;
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--color-primary: #3b82f6;
}
/* Respect motion preferences */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Button micro-interaction */
.btn {
padding: 0.75rem 1.5rem;
border: 0;
border-radius: 6px;
background: var(--color-primary);
color: #fff;
font-weight: 600;
transition: transform var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.15);
}
.btn:active {
transform: translateY(0);
transition-duration: 100ms;
}
/* Staggered fade-in */
.content-section {
animation: fadeInUp var(--duration) var(--ease-out) backwards;
}
.content-section:nth-child(1) {
animation-delay: 0.1s;
}
.content-section:nth-child(2) {
animation-delay: 0.2s;
}
.content-section:nth-child(3) {
animation-delay: 0.3s;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
<button class="btn">Primary Action</button>
<section class="content-section">
<h2>Section A</h2>
<p>Appears immediately.</p>
</section>
<section class="content-section">
<h2>Section B</h2>
<p>Appears after 0.1s.</p>
</section>
<section class="content-section">
<h2>Section C</h2>
<p>Appears after 0.2s.</p>
</section>
Teacher note: Demo first. Ask: What did we animate? Why? What if motion is reduced?
🎯 Learning Objectives
- Understand animation as communication - Motion with purpose, not decoration
- Master CSS animation techniques - Transitions, keyframes, transforms, and timing functions
- Apply performance best practices - Hardware acceleration, will-change, and reduced motion
- Design accessible animations - Respecting user preferences and cognitive load
- Think critically about motion - When animation enhances vs. distracts from user experience
- Build production-ready animations - From micro-interactions to complex sequences
🎭 Critical Perspective: Animation as Language
“Animation is not about making things move. It’s about making things feel alive.” — Anonymous UX Designer
Before we code a single transition, let’s pause and think critically about why we animate:
The Good: Motion That Serves
- Provides feedback - Button pressed, form submitted, data loading
- Guides attention - “Look here next” spatial navigation
- Reveals relationships - Parent-child, cause-effect, before-after
- Delights users - Personality, brand voice, emotional connection
- Reduces cognitive load - Smooth transitions prevent disorientation
The Bad: Motion That Harms
- Distracts from content - Gratuitous spinners, bouncing everything
- Triggers vestibular disorders - Parallax, rotation without
prefers-reduced-motion - Slows interaction - Long animations blocking user progress
- Drains battery - Inefficient animations on mobile devices
- Excludes users - No fallback for motion sensitivity
🤔 Atelier Critical Prompt #1
Before animating anything in your project, ask:
- Purpose: What user need does this motion serve?
- Alternatives: Could static design communicate the same thing?
- Cost: What’s the performance/accessibility trade-off?
- Meaning: Does this motion align with my project’s voice?
Document your answers in your project journal (commit message or DESIGN-DECISIONS.md).
📜 The Evolution of Web Animation
1995-2005 ━━━━━━━━━━━━━━━━━ Dark Ages
│ • <blink>, <marquee> tags (deprecated)
│ • GIF animations (limited, heavy)
│ • Flash (proprietary, inaccessible)
│
2006-2010 ━━━━━━━━━━━━━━━━━ jQuery Era
│ • .animate(), .slideDown()
│ • JavaScript-driven (performance issues)
│ • Cross-browser inconsistency
│
2011-2015 ━━━━━━━━━━━━━━━━━ CSS3 Revolution
│ • transition, @keyframes
│ • transform (GPU-accelerated)
│ • animation-* properties
│
2016-2020 ━━━━━━━━━━━━━━━━━ Modern Declarative
│ • Web Animations API (WAAPI)
│ • Intersection Observer
│ • CSS Variables in animations
│ • Houdini API experiments
│
2021-NOW ━━━━━━━━━━━━━━━━━ Mature & Accessible
• View Transitions API
• Scroll-driven animations
• prefers-reduced-motion standard
• Performance budgets for motion
“CSS animations won because they put motion control where it belongs: in the presentation layer, not the behavior layer.” — CSS-Tricks, adapted
🎓 Theory: The 3 Pillars of CSS Animation
Pillar 1: Transitions (State → State)
What is a “state”?
A “state” is simply how an element appears or behaves at a certain moment—such as default, hovered, focused, or active. Transitions help you animate the visual change when an element moves from one state to another (for example, from its normal style to a hovered style).
Create your own sandbox:
For all code examples and hands-on practice with animation in this lesson, create a file at /animations/index.html in your project. Use it as your sandbox for experiments!
Example HTML (copy this into /animations/index.html):
<button class="button">Hover me!</button>
Now try adding the CSS below to your sandbox to experiment with transitions.
What: Smooth interpolation between two property values.
When: User-triggered states (hover, focus, active) or class changes.
Syntax:
.element {
transition: property duration timing-function delay;
}
Example:
.button {
background: #3b82f6;
color: white;
transition: background 0.3s ease, transform 0.15s ease-out;
}
.button:hover {
background: #2563eb;
transform: translateY(-2px);
}
.button:active {
transform: translateY(0);
transition-duration: 0.1s; /* Faster on click */
}
🔑 Key Insight: Transitions are reactive - they need a trigger (hover, class change, JS state update).
Pillar 2: Keyframes (Multi-Step Choreography)
What: Define intermediate steps in an animation sequence.
When: Complex animations, looping effects, independent from user interaction.
Syntax:
@keyframes animation-name {
from {
/* or 0% */
}
25% {
}
50% {
}
to {
/* or 100% */
}
}
.element {
animation: animation-name duration timing-function delay iteration-count direction fill-mode;
}
Example - Fade-in on page load:
<section class="content-section">
<h2>Welcome</h2>
<p>This block fades in and moves up on load.</p>
</section>
<section class="content-section">
<h2>Features</h2>
<p>Stagger with increasing delays.</p>
</section>
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.content-section {
animation: fadeInUp 0.6s ease-out forwards;
}
/* Stagger with delays */
.content-section:nth-child(1) {
animation-delay: 0.1s;
}
.content-section:nth-child(2) {
animation-delay: 0.2s;
}
.content-section:nth-child(3) {
animation-delay: 0.3s;
}
🔑 Key Insight: Keyframes are proactive - they run automatically when applied.
Pillar 3: Transforms (Spatial Manipulation)
What: Move, rotate, scale, or skew elements without triggering layout reflow.
Why: GPU-accelerated = smooth 60fps performance.
Core Functions:
transform: translateX(100px); /* Move horizontally */
transform: translateY(-50px); /* Move vertically */
transform: translate(50px, -20px); /* Both at once */
transform: scale(1.2); /* Grow 20% */
transform: scaleX(0.8); /* Squish horizontally */
transform: rotate(45deg); /* Rotate clockwise */
transform: skewX(10deg); /* Tilt */
/* Combine multiple transforms */
transform: translateX(50px) rotate(15deg) scale(1.1);
Example - Card lift effect:
<article class="card">
<h3>Card Title</h3>
<p>Hover me to see lift and shadow.</p>
</article>
.card {
transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1), /* Bounce */ box-shadow 0.3s ease;
}
.card:hover {
transform: translateY(-8px) scale(1.02);
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
🔑 Key Insight: Always use transform and opacity for animations - they’re compositor-only (no repaints).
⚡ Performance: The “Will-Change” Optimization
Problem: First frame of animation can stutter (browser preparing GPU layer).
Solution: Tell browser in advance what will animate.
.button {
will-change: transform, opacity;
/* Browser creates GPU layer immediately */
}
/* ⚠️ Don't overuse! */
.everything {
will-change: auto; /* Default - only optimize what animates */
}
Best Practice:
/* Add will-change only when needed */
.card:hover,
.card:focus-within {
will-change: transform;
}
Forbidden:
/* ❌ BAD - wastes memory on every element */
* {
will-change: transform;
}
♿ Accessibility: Respecting User Motion Preferences
Reality: ~35% of users experience motion sickness from animations (Vestibular Disorders Association).
Solution: prefers-reduced-motion media query.
/* Full animation for users who can tolerate it */
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loader {
animation: spin 1s linear infinite;
}
/* Respect user preference */
@media (prefers-reduced-motion: reduce) {
.loader {
animation: none;
/* Provide static alternative */
opacity: 0.6;
}
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
Progressive Enhancement Approach:
/* Start with no motion (safe default) */
.element {
opacity: 1;
}
/* Add motion only if user allows */
@media (prefers-reduced-motion: no-preference) {
.element {
animation: fadeIn 0.5s ease;
}
}
🤔 Atelier Critical Prompt #2
Motion is not inclusive by default.
Test your animations with:
- System settings: Enable “Reduce Motion” in OS accessibility
- DevTools: Chrome/Firefox have motion emulation
- Real users: Ask someone with vestibular sensitivity
Commit requirement: All animations MUST have prefers-reduced-motion fallback.
🏗️ Atelier Workshop: Building Animation Systems
✅ Pure CSS - No Framework Required
All examples use standard CSS. No Tailwind, Bootstrap, or JavaScript dependencies (unless explicitly marked).
Setup: Animation Utilities Starter
Create src/styles/animations.css in your project:
/* ============================================
ANIMATION SYSTEM - Pure CSS
Design Tokens + Reusable Animations
Framework-agnostic, works everywhere
============================================ */
/* --- Timing Tokens --- */
:root {
--duration-instant: 100ms;
--duration-fast: 200ms;
--duration-normal: 300ms;
--duration-slow: 500ms;
--duration-slower: 800ms;
--ease-in: cubic-bezier(0.4, 0, 1, 1);
--ease-out: cubic-bezier(0, 0, 0.2, 1);
--ease-in-out: cubic-bezier(0.4, 0, 0.2, 1);
--ease-bounce: cubic-bezier(0.68, -0.55, 0.265, 1.55);
--ease-elastic: cubic-bezier(0.68, -0.35, 0.265, 1.35);
/* Color tokens (framework-agnostic) */
--color-primary: #3b82f6;
--color-gray-100: #f3f4f6;
--color-gray-700: #374151;
--color-gray-900: #111827;
}
/* --- Base Reset --- */
/*
The following media query targets users who have enabled "Reduce Motion" in their operating system or browser preferences.
When (prefers-reduced-motion: reduce) is true, all CSS animations and transitions are effectively disabled for better accessibility.
*/
@media (prefers-reduced-motion: reduce) {
_,
_::before,
\*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* --- Utility Classes --- */
.fade-in {
animation: fadeIn var(--duration-normal) var(--ease-out);
}
.slide-in-up {
animation: slideInUp var(--duration-slow) var(--ease-out);
}
.scale-in {
animation: scaleIn var(--duration-fast) var(--ease-bounce);
}
/* --- Keyframe Library --- */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes scaleIn {
from {
opacity: 0;
transform: scale(0.8);
}
to {
opacity: 1;
transform: scale(1);
}
}
@keyframes rotate {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
Import in your src/main.css:
@import './styles/animations.css';
🎨 Exercise 1: Micro-Interactions (Button States)
✅ Pure CSS - No JavaScript Required
Goal: Add delightful feedback to buttons.
Where: styles/button.css (or any HTML page + CSS file)
Step 1: HTML
<button class="btn btn-primary">Primary Button</button>
<button class="btn btn-secondary">Secondary Button</button>
<button class="btn btn-primary" disabled>Disabled Button</button>
<p style="font-size:13px;color:#374151;margin-top:6px;">Open <a href="./examples/button.html">button example</a> in a new tab if the embed is blocked.</p>
Step 2: CSS (Base + Variants)
/* styles/button.css */
/* Base button styles */
.btn {
position: relative;
overflow: hidden;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: transform var(--duration-fast) var(--ease-out), box-shadow var(--duration-fast) var(--ease-out);
}
/* Variants */
.btn-primary {
background-color: var(--color-primary);
color: white;
}
.btn-secondary {
background-color: var(--color-gray-100);
color: var(--color-gray-900);
}
/* Interactions */
.btn:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.btn:active:not(:disabled) {
transform: translateY(0);
transition-duration: var(--duration-instant);
}
/* Focus ring animation */
.btn:focus-visible {
outline: 2px solid currentColor;
outline-offset: 2px;
animation: pulseRing 0.6s ease-out;
}
@keyframes pulseRing {
0% {
outline-offset: 2px;
}
100% {
outline-offset: 6px;
outline-color: transparent;
}
}
/* Ripple effect (optional) */
.btn::after {
content: '';
position: absolute;
inset: 0;
background: rgba(255, 255, 255, 0.3);
border-radius: inherit;
transform: scale(0);
opacity: 0;
}
.btn:active:not(:disabled)::after {
animation: ripple 0.6s ease-out;
}
@keyframes ripple {
0% {
transform: scale(0);
opacity: 1;
}
100% {
transform: scale(1);
opacity: 0;
}
}
/* Disabled */
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Accessibility: reduced motion */
@media (prefers-reduced-motion: reduce) {
.btn {
transition-duration: 0.01ms !important;
}
.btn::after,
.btn:focus-visible {
animation: none !important;
}
}
Teacher Notes
- Demonstrate hover/active/focus with keyboard and mouse.
- Ask: Which properties animate? Why
transformand nottop? - Toggle OS “Reduce Motion” and verify no distracting motion.
- Optional: Discuss color-contrast and focus visibility.
🎨 Exercise 2: Page Load Animations (Staggered Fade-In)
✅ Pure CSS - No JavaScript Required
Goal: Content gracefully appears on page load using only CSS.
Where: Any HTML page + styles/animations.css
HTML:
<main>
<section class="content-section">
<h2>About Me</h2>
<p>First section fades in immediately...</p>
</section>
<section class="content-section">
<h2>My Work</h2>
<p>Second section fades in 0.1s later...</p>
</section>
<section class="content-section">
<h2>Contact</h2>
<p>Third section fades in 0.2s later...</p>
</section>
</main>
CSS:
/* Fade-in animation for all sections */
.content-section {
animation: fadeInUp var(--duration-slow) var(--ease-out) backwards;
}
/* Stagger delays */
.content-section:nth-child(1) {
animation-delay: 0.1s;
}
.content-section:nth-child(2) {
animation-delay: 0.2s;
}
.content-section:nth-child(3) {
animation-delay: 0.3s;
}
.content-section:nth-child(4) {
animation-delay: 0.4s;
}
.content-section:nth-child(5) {
animation-delay: 0.5s;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Reduced motion: instant appearance */
@media (prefers-reduced-motion: reduce) {
.content-section {
animation: none;
}
}
Teacher Notes
- Keep it simple: delay only; do not over-animate.
- Explain
backwardsfill mode prevents initial flicker. - Compare with/without
prefers-reduced-motionenabled. - Extension: Optional JS (Intersection Observer) only if scroll-triggered is required.
⚠️ Optional JavaScript Enhancement - Scroll-Triggered Animations (Advanced)
Requires: Intersection Observer API (browser support: 95%+ globally)
When to use: Only if you need animations triggered by scrolling into view (not on page load)
JavaScript (optional enhancement):
// utils/scroll-animations.js
function observeAnimations() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
entry.target.classList.add('animate-in');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1 }
);
document.querySelectorAll('[data-animate]').forEach((el) => {
observer.observe(el);
});
}
// Initialize when DOM is ready
document.addEventListener('DOMContentLoaded', () => {
observeAnimations();
});
HTML (with data-animate attribute):
<section class="content-section" data-animate>
<h2>About Me</h2>
<p>Content that fades in when scrolled into view...</p>
</section>
CSS (for JavaScript-triggered animations):
/* Initially hidden */
[data-animate] {
opacity: 0;
transform: translateY(30px);
}
/* Animate when .animate-in class is added by JavaScript */
[data-animate].animate-in {
animation: fadeInUp 0.6s ease-out forwards;
}
Note: The pure CSS approach (without JavaScript) is recommended for most cases. Use JavaScript only when you specifically need scroll-triggered animations.
🎨 Exercise 3: Loading States (Skeleton Screens & Spinners)
✅ Pure CSS - No JavaScript Required
Goal: Show progress without frustrating users.
Option A: Skeleton Screen (Preferred for Content)
HTML:
<article class="skeleton-card">
<div class="skeleton skeleton-image"></div>
<div class="skeleton skeleton-title"></div>
<div class="skeleton skeleton-text"></div>
<div class="skeleton skeleton-text"></div>
</article>
CSS:
/* Base skeleton shimmer animation */
.skeleton {
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
border-radius: 4px;
}
@keyframes shimmer {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
/* Skeleton variants */
.skeleton-image {
width: 100%;
height: 200px;
}
.skeleton-title {
width: 70%;
height: 24px;
margin: 1rem 0 0.5rem;
}
.skeleton-text {
width: 100%;
height: 14px;
margin-bottom: 0.5rem;
}
/* Complete skeleton card */
.skeleton-card {
padding: 1rem;
border: 1px solid #e5e7eb;
border-radius: 8px;
}
/* Respect motion preferences */
@media (prefers-reduced-motion: reduce) {
.skeleton {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.6;
}
}
}
Option B: Spinner (Use Sparingly)
Use only for short waits (<3 seconds). Prefer skeleton screens for content loading.
HTML:
<div class="spinner" role="status" aria-label="Loading content"></div>
<p class="sr-only">Loading content, please wait...</p>
CSS:
.spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(0, 0, 0, 0.1);
border-left-color: var(--color-primary, #3b82f6);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* Screen reader only text */
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
/* Respect motion preference */
@media (prefers-reduced-motion: reduce) {
.spinner {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.5;
}
}
}
Teacher Notes
- Prefer skeletons for content; spinners only for brief actions (<3s).
- Ask: How can we avoid loaders? (optimistic UI, prefetching, streaming)
- Ensure color contrast and SR-only text is present for accessibility.
- Verify reduced motion still provides clear feedback without rotation.
🎨 Exercise 4: SVG Animations (Icons & Logos)
✅ Pure CSS + SVG - No JavaScript Required
Goal: Animate SVG graphics using pure CSS for professional, scalable animations.
SVG Path Drawing Effect
HTML (inline SVG):
<svg class="logo-animated" width="200" height="200" viewBox="0 0 200 200">
<path class="logo-path" d="M 50 100 Q 100 50 150 100 T 250 100" fill="none" stroke="currentColor" stroke-width="3" />
</svg>
CSS:
.logo-path {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: draw 2s ease-out forwards;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
/* Respect motion preferences */
@media (prefers-reduced-motion: reduce) {
.logo-path {
animation: none;
stroke-dashoffset: 0;
}
}
How it works:
stroke-dasharraycreates dashed strokestroke-dashoffsethides the stroke initially- Animation brings
stroke-dashoffsetto 0, “drawing” the path
SVG Icon Morphing
HTML:
<svg class="icon-morph" width="48" height="48" viewBox="0 0 24 24">
<circle class="circle-to-square" cx="12" cy="12" r="8" fill="currentColor" />
</svg>
CSS:
.circle-to-square {
animation: morph-shape 3s ease-in-out infinite;
transform-origin: center;
}
@keyframes morph-shape {
0%,
100% {
d: path('M12,4 a8,8 0 1,0 0,16 a8,8 0 1,0 0,-16'); /* Circle */
}
50% {
d: path('M4,4 h16 v16 h-16 z'); /* Square */
}
}
Animated SVG Background Pattern
HTML:
<div class="hero-section">
<svg class="bg-pattern" width="100%" height="100%">
<defs>
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<circle cx="20" cy="20" r="2" fill="rgba(59, 130, 246, 0.1)" />
</pattern>
</defs>
<rect width="100%" height="100%" fill="url(#grid)" />
</svg>
<h1>Content over animated background</h1>
</div>
CSS:
.bg-pattern {
position: absolute;
inset: 0;
z-index: -1;
animation: pattern-slide 20s linear infinite;
}
@keyframes pattern-slide {
to {
transform: translate(40px, 40px);
}
}
/* Hero with relative positioning */
.hero-section {
position: relative;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
🎯 Advanced Practice: Morphing Shapes (CSS clip-path)
HTML:
<button class="morph-button">Hover to morph</button>
<div class="shape"></div>
.morph-button {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
transition: clip-path 0.5s var(--ease-in-out);
}
.morph-button:hover {
clip-path: polygon(0 10%, 100% 0, 100% 90%, 0 100%);
}
/* Circle to square morph */
.shape {
width: 100px;
height: 100px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
clip-path: circle(50%);
transition: clip-path 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.shape:hover {
clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
}
Typewriter Effect
HTML:
<h2 class="typewriter">Design with motion, not distraction.</h2>
.typewriter {
font-family: monospace;
overflow: hidden;
border-right: 2px solid;
white-space: nowrap;
animation: typing 3.5s steps(40, end), blink-caret 0.75s step-end infinite;
}
@keyframes typing {
from {
width: 0;
}
to {
width: 100%;
}
}
@keyframes blink-caret {
from,
to {
border-color: transparent;
}
50% {
border-color: currentColor;
}
}
Glitch Effect
HTML:
<h2 class="glitch" data-text="GLITCH">GLITCH</h2>
.glitch {
position: relative;
animation: glitch 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94) both infinite;
}
.glitch::before,
.glitch::after {
content: attr(data-text);
position: absolute;
inset: 0;
}
.glitch::before {
left: 2px;
text-shadow: -2px 0 #ff00de;
clip: rect(24px, 550px, 90px, 0);
animation: glitch-anim 2s infinite linear alternate-reverse;
}
.glitch::after {
left: -2px;
text-shadow: -2px 0 #00fff9, 2px 2px #ff00de;
animation: glitch-anim 2s infinite linear alternate-reverse;
}
@keyframes glitch-anim {
0% {
clip: rect(13px, 9999px, 94px, 0);
}
5% {
clip: rect(92px, 9999px, 61px, 0);
}
10% {
clip: rect(69px, 9999px, 40px, 0);
}
100% {
clip: rect(76px, 9999px, 19px, 0);
}
}
Animated Gradient Text
HTML:
<h2 class="gradient-text">Animated Gradient</h2>
.gradient-text {
background: linear-gradient(45deg, #12c2e9, #c471ed, #f64f59, #12c2e9);
background-size: 300% 300%;
background-clip: text;
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: gradientShift 3s ease infinite;
}
@keyframes gradientShift {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
🎯 Advanced Practice: Scroll-Driven Animations (Modern CSS)
✅ Pure CSS - New in 2024!
Browser support: Chrome 115+, Safari 17+ (as of 2025) - check caniuse.com > When to use: Progressive enhancement for modern browsers
Goal: Animate based on scroll position WITHOUT JavaScript!
HTML:
<div class="hero">
<h1>Scroll to see parallax effect</h1>
</div>
<div class="reading-progress"></div>
Check live demo: Scroll-driven animations demo
Related demo (parallax): 01 — Parallax Scrolling (modern-web-design-trends). Note: the classic background-attachment: fixed parallax works on desktop but is unreliable on many mobile browsers — prefer transform-based or CSS scroll() timeline techniques and always provide a prefers-reduced-motion fallback for accessibility.
CSS:
/* Parallax background */
.hero {
animation: parallax linear;
animation-timeline: scroll();
}
@keyframes parallax {
to {
transform: translateY(50vh);
}
}
/* Reading progress bar */
.reading-progress {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(to right, #667eea, #764ba2);
transform-origin: left;
animation: progressBar linear;
animation-timeline: scroll();
}
@keyframes progressBar {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
⚠️ JavaScript Fallback for Older Browsers (Optional)
Use only if you need to support browsers without
animation-timelinesupport
// utils/scroll-fallback.js
if (!CSS.supports('animation-timeline: scroll()')) {
// JavaScript fallback for older browsers
window.addEventListener('scroll', () => {
const scrolled = window.scrollY;
const total = document.body.scrollHeight - window.innerHeight;
const progress = scrolled / total;
document.querySelector('.reading-progress').style.transform = `scaleX(${progress})`;
});
}
🏆 Expert Challenge: View Transitions API
⚠️ JavaScript Required - Cutting-Edge Browser API
Browser support: Chrome 111+, Edge 111+ (as of 2025) When to use: Advanced SPA page transitions (not achievable with CSS alone)
The Future: Smooth transitions between pages in SPAs and MPAs!
// Check for browser support
if (document.startViewTransition) {
document.startViewTransition(() => {
// Update DOM (e.g., navigate to new view)
renderNewView();
});
} else {
// Fallback: instant navigation
renderNewView();
}
CSS:
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.5s;
}
/* Custom transitions for specific elements */
.hero-image {
view-transition-name: hero;
}
::view-transition-old(hero),
::view-transition-new(hero) {
animation-duration: 1s;
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
📚 Animation Performance Checklist
Before committing animations to your project:
- Only animate
transformandopacity(GPU-accelerated) - Use
will-changesparingly (only on hover/interaction) - Implement
prefers-reduced-motion(accessibility requirement) - Test on mobile devices (battery/performance impact)
- Avoid animating
width/height/top/left(triggers layout) - Use
requestAnimationFrame()for JS animations - Debounce scroll listeners (if using JavaScript)
- Set animation budgets (max 3-4 simultaneous animations)
Performance testing:
# Chrome DevTools > Performance
# Record → Interact → Check for:
# - Frame drops (below 60fps)
# - Layout/Paint operations
# - Memory leaks (increasing heap)
🎓 Atelier Reflection: When NOT to Animate
Animation is a tool, not a requirement. Consider static design when:
- Content is priority - Don’t distract from reading/critical tasks
- Low-end devices - Respect limited CPU/battery
- Professional contexts - Banking, healthcare, legal sites favor trust over delight
- Accessibility concerns - Motion sensitivity, cognitive load, screen readers
- Performance budget - You’ve maxed out your bundle size / load time
🤔 Atelier Critical Prompt #4
“Move fast and break things” is a lie.
In your project journal, reflect:
- Which animations did you remove after testing?
- What static alternatives worked better?
- How did users with disabilities experience your motion?
Aim: 80% of your UX should work perfectly with CSS and animations disabled.
🌍 Real-World Case Studies
Good: Stripe’s Subtle Feedback
- Button hover: 2px lift + shadow (50ms)
- Form validation: Green checkmark fade-in (200ms)
- Page transitions: Cross-fade (300ms)
- Why it works: Fast, purposeful, enhances trust
Bad: Early 2010s Portfolio Sites
- Every element rotating on scroll
- Parallax on parallax
- Text typing character-by-character
- Why it failed: Slow, distracting, nausea-inducing
Excellent: Apple Product Pages
- Scroll-driven product reveals
- Precise timing (synchronized with narrative)
- High-performance (only transform/opacity)
- Why it’s exemplary: Motion serves storytelling, not ego
📝 Assignment: Animate Your Portfolio
Requirements:
-
Add 3 types of animations to your project:
- Micro-interaction (button/link hover)
- Page load animation (staggered fade-in)
- Loading state (skeleton or spinner)
-
Performance:
- Only
transformandopacity - Test on mobile (no jank)
- Document performance metrics
- Only
-
Accessibility:
- Implement
prefers-reduced-motion - Test with motion disabled
- Ensure focus states are clear
- Implement
-
Critical Reflection:
- Write 2-3 paragraphs in
DESIGN-DECISIONS.md:- Why did you choose these animations?
- What alternatives did you consider?
- How do they improve UX?
- Write 2-3 paragraphs in
Commit:
git add src/styles/animations.css src/views/ DESIGN-DECISIONS.md
git commit -m "feat: Add animation system to portfolio
Animations:
- Button micro-interactions (hover lift + ripple)
- Staggered page load fade-in (0.1s delays)
- Skeleton loading for project cards
Performance:
- All animations use transform/opacity only
- will-change on interactive elements only
- Tested: 60fps on iPhone SE 2020
Accessibility:
- prefers-reduced-motion fallback (instant transitions)
- Focus indicators pulse animation
- Keyboard navigation tested
Reflection in DESIGN-DECISIONS.md:
- Motion serves feedback, not decoration
- Removed parallax after vestibular testing
- Static design works better for critical CTA"
🎯 Advanced Optional Challenges
For Design-Focused Students:
Challenge 1: Emotion Through Motion
- Create 3 button styles that convey different emotions:
- Playful (bounce, wiggle)
- Serious (subtle, precise)
- Energetic (pulse, scale)
- Deliverable: Codepen + 1-page design rationale
Challenge 2: Brand Animation System
- Define motion design tokens for a brand (Airbnb, Nike, your project)
- Deliverable: CSS variables for durations, easings, patterns
For Code-Focused Students:
Challenge 3: Web Animations API (WAAPI) ⚠️ JavaScript Required
When to use: Complex animations requiring programmatic control (play/pause/reverse)
- Rewrite a CSS animation using JavaScript:
// utils/waapi-example.js
const element = document.querySelector('.animated-element');
const animation = element.animate(
[
{ transform: 'scale(1)', opacity: 1 },
{ transform: 'scale(1.2)', opacity: 0.8 },
{ transform: 'scale(1)', opacity: 1 },
],
{
duration: 600,
easing: 'cubic-bezier(0.68, -0.55, 0.265, 1.55)',
iterations: Infinity,
}
);
// Control animations programmatically
animation.pause();
animation.play();
animation.reverse();
- Deliverable: Interactive demo + performance comparison (CSS vs WAAPI)
- Compare: CPU usage, frame rate, bundle size
Challenge 4: Intersection Observer Animations ⚠️ JavaScript Required
Pure CSS alternative: Use
animation-delayon page load (see Exercise 2) Use JavaScript only if: You need scroll-triggered animations (not load animations)
- Animate sections only when visible in viewport
- Deliverable: Reusable
animateOnScroll()utility function
For Advanced Students:
Challenge 5: Physics-Based Animations ⚠️ JavaScript + Library Required
Libraries: Popmotion, Framer Motion, React Spring Bundle size: ~10-30KB (consider performance trade-offs)
- Implement spring physics for natural movement
- Deliverable: Natural-feeling drag-and-drop interface
- Reflect: Was the bundle size worth it? Could CSS have achieved similar results?
Challenge 6: SVG Path Animations ✅ Pure CSS
Recommended: This is achievable with pure CSS! (see Exercise 4)
- Animate SVG
<path>usingstroke-dasharrayandstroke-dashoffset - Deliverable: Animated logo or icon that draws on page load
- Bonus: Add
prefers-reduced-motionfallback
📚 Further Learning
Essential Reading
- Material Design Motion: motion.material.io
- Refactoring UI: Animation chapter (design principles)
- Val Head: Designing Interface Animation (O’Reilly)
- Rachel Nabors: Animation at Work (A Book Apart)
Tools & Playgrounds
- Cubic Bezier: cubic-bezier.com - Easing curve editor
- Animista: animista.net - CSS animation library
- Lottie: airbnb.design/lottie - After Effects to web
- GSAP: greensock.com/gsap - JavaScript animation library
Standards & Specs
- MDN: CSS Animations: mdn.io/css-animations
- WCAG 2.1: Animation & Motion (Success Criterion 2.3.3)
- View Transitions API: github.com/WICG/view-transitions
🎓 Atelier Final Reflection
“The best animation is invisible. Users should feel the flow, not study the technique.” — Anonymous
By completing this lesson, you’ve learned:
- ✅ Technical mastery - CSS transitions, keyframes, transforms
- ✅ Performance awareness - GPU acceleration, will-change, reduced motion
- ✅ Accessibility commitment - Motion preferences, cognitive load
- ✅ Critical thinking - When to animate (and when not to)
- ✅ Professional practice - Design systems, documentation, testing
You are now equipped to make the web more delightful AND more inclusive.
📝 Commit Your Learning
git add .
git commit -m "feat: Complete Web Animations mastery
Theory:
- Understand transitions, keyframes, transforms
- Master timing functions and easing
- Learn performance optimization techniques
Practice:
- Built animation system with design tokens
- Implemented micro-interactions (buttons)
- Created loading states (skeleton, spinner)
- Added page load animations (staggered)
Accessibility:
- All animations respect prefers-reduced-motion
- Tested with motion disabled
- Documented accessibility decisions
Critical Reflection:
- Animation serves purpose, not decoration
- Performance budget enforced
- Users with vestibular disorders considered
Portfolio: Now has delightful, accessible motion ✨"
🎨 Atelier Philosophy: “Animation is not decoration. It is communication. Move with purpose. Delight with restraint. Always ask: does this motion serve my users, or only my ego?”
— Prof. Rubén Vega Balbás, PhD
Next Steps:
- Review JavaScript Modules to organize animation code modularly
- Study accessibility guidelines (WCAG 2.1) for motion and animation
- Explore performance optimization tools (Chrome DevTools, Lighthouse)
- Practice SVG animation techniques for logos and icons
- Learn more about CSS
scroll()timeline for scroll-driven animations
Let’s make the web a beautiful AND usable virtual ecosystem!