By the end of this lesson, you will be able to:
Animations are more than just visual flair—they improve user experience by providing feedback, guiding attention, and making interfaces feel responsive and alive.
Benefits of CSS Animations:
When to Use Animations:
Transitions allow you to smoothly animate changes between CSS property values. When a property changes (like on hover), the transition interpolates between the old and new values.
/* Transition a specific property */
.button {
background-color: blue;
transition: background-color 0.3s ease;
}
.button:hover {
background-color: green;
}
/* Transition all properties */
.card {
transition: all 0.3s ease;
}
/* Transition multiple properties with different timings */
.box {
transition:
transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55),
opacity 0.2s ease,
background-color 0.5s linear;
}
.element {
/* Shorthand: property duration timing-function delay */
transition: transform 0.3s ease-in-out 0.1s;
/* Or use individual properties */
transition-property: transform;
transition-duration: 0.3s;
transition-timing-function: ease-in-out;
transition-delay: 0.1s;
}
Timing functions control the pace of an animation. They determine how intermediate values are calculated during the transition.
/* Linear - constant speed */
transition: all 0.3s linear;
/* Ease - slow start, fast middle, slow end (default) */
transition: all 0.3s ease;
/* Ease-in - slow start, fast end */
transition: all 0.3s ease-in;
/* Ease-out - fast start, slow end */
transition: all 0.3s ease-out;
/* Ease-in-out - slow start and end */
transition: all 0.3s ease-in-out;
/* Create custom easing curves */
.bounce {
transition: transform 0.5s cubic-bezier(0.68, -0.55, 0.265, 1.55);
}
.smooth-slide {
transition: transform 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* Use https://cubic-bezier.com to create custom curves */
/* Jump in discrete steps (useful for sprite animations) */
.sprite {
transition: background-position 0.8s steps(8);
}
Transforms allow you to move, rotate, scale, and skew elements in 2D space. Transforms are performant because they don't trigger layout recalculation.
/* Translate - move element */
transform: translate(50px, 100px); /* X, Y */
transform: translateX(50px);
transform: translateY(100px);
/* Scale - resize element */
transform: scale(1.5); /* Uniform scaling */
transform: scale(2, 0.5); /* X, Y */
transform: scaleX(2);
transform: scaleY(0.5);
/* Rotate - rotate element */
transform: rotate(45deg);
transform: rotate(-30deg);
/* Skew - slant element */
transform: skew(20deg, 10deg); /* X, Y */
transform: skewX(20deg);
transform: skewY(10deg);
/* Multiple transforms are applied right to left */
.element {
/* Rotate, then translate, then scale */
transform: scale(1.2) translate(50px, 0) rotate(15deg);
}
/* Transform origin - change the pivot point */
.element {
transform-origin: top left; /* Default is center */
transform: rotate(45deg);
}
I scale and lift on hover
Keyframe animations allow you to create complex, multi-stage animations with precise control over each stage. Unlike transitions, animations can run automatically without a trigger.
/* Using from/to keywords */
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
/* Using percentages for multi-stage animation */
@keyframes bounce {
0% {
transform: translateY(0);
}
50% {
transform: translateY(-20px);
}
100% {
transform: translateY(0);
}
}
/* Complex animation with multiple properties */
@keyframes slideInBounce {
0% {
opacity: 0;
transform: translateX(-100px) scale(0.8);
}
60% {
transform: translateX(10px) scale(1.1);
}
100% {
opacity: 1;
transform: translateX(0) scale(1);
}
}
/* Shorthand syntax */
.element {
animation: fadeIn 0.5s ease-in-out 0.2s 1 normal forwards;
/* name | duration | timing | delay | iteration | direction | fill-mode */
}
/* Individual properties */
.element {
animation-name: slideInBounce;
animation-duration: 0.8s;
animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
animation-delay: 0s;
animation-iteration-count: 1; /* or 'infinite' */
animation-direction: normal; /* or 'reverse', 'alternate', 'alternate-reverse' */
animation-fill-mode: forwards; /* or 'backwards', 'both', 'none' */
animation-play-state: running; /* or 'paused' */
}
Controls what happens before and after the animation runs:
/* none - no styles applied outside animation */
animation-fill-mode: none;
/* forwards - retain final keyframe styles */
animation-fill-mode: forwards;
/* backwards - apply initial keyframe styles during delay */
animation-fill-mode: backwards;
/* both - apply both forwards and backwards */
animation-fill-mode: both;
/* normal - play forward */
animation-direction: normal;
/* reverse - play backward */
animation-direction: reverse;
/* alternate - forward then backward */
animation-direction: alternate;
/* alternate-reverse - backward then forward */
animation-direction: alternate-reverse;
/* Run once */
animation-iteration-count: 1;
/* Run 3 times */
animation-iteration-count: 3;
/* Run forever */
animation-iteration-count: infinite;
CSS 3D transforms allow you to create depth and dimension. Combined with perspective, you can create truly three-dimensional effects.
/* Perspective on parent creates shared 3D space */
.scene {
perspective: 1000px; /* Lower = more extreme perspective */
}
/* Perspective on element itself */
.element {
transform: perspective(1000px) rotateY(45deg);
}
/* 3D Translation */
transform: translate3d(50px, 100px, -200px);
transform: translateZ(-200px);
/* 3D Rotation */
transform: rotateX(45deg); /* Rotate around X axis */
transform: rotateY(45deg); /* Rotate around Y axis */
transform: rotateZ(45deg); /* Same as rotate() */
transform: rotate3d(1, 1, 0, 45deg); /* Custom axis */
/* 3D Scale */
transform: scale3d(1.5, 1.5, 2);
transform: scaleZ(2);
/* Preserve 3D - children maintain 3D positioning */
.parent {
transform-style: preserve-3d;
}
/* Flat (default) - children are flattened to parent plane */
.parent {
transform-style: flat;
}
/* Hide element when rotated away from viewer */
.card-face {
backface-visibility: hidden;
}
/* Show backface (default) */
.card-face {
backface-visibility: visible;
}
Hover to flip
The "checkbox hack" is a technique that uses hidden checkboxes combined with
the :checked pseudo-class to create interactive components without
JavaScript.
<input type="checkbox" id="toggle" class="toggle-checkbox">
<label for="toggle" class="toggle-label">Click to toggle</label>
<div class="toggle-content">
This content is toggled!
</div>
/* Hide the checkbox */
.toggle-checkbox {
display: none;
}
/* Style the label as a button */
.toggle-label {
display: inline-block;
padding: 10px 20px;
background: #3b82f6;
color: white;
cursor: pointer;
border-radius: 4px;
}
/* Hide content by default */
.toggle-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease;
}
/* Show content when checkbox is checked */
.toggle-checkbox:checked ~ .toggle-content {
max-height: 500px; /* Large enough for content */
}
/* Change label when checked */
.toggle-checkbox:checked ~ .toggle-label {
background: #10b981;
}
The :target pseudo-class selects an element when the URL's fragment
identifier (hash) matches that element's ID. This enables pure CSS modals, tabs,
and navigation.
<a href="#section1">Go to Section 1</a>
<a href="#section2">Go to Section 2</a>
<div id="section1">Section 1 Content</div>
<div id="section2">Section 2 Content</div>
/* Hide all sections by default */
div[id^="section"] {
display: none;
}
/* Show the targeted section */
div[id^="section"]:target {
display: block;
animation: fadeIn 0.3s ease;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* Hide modal by default */
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
align-items: center;
justify-content: center;
}
/* Show modal when targeted */
.modal:target {
display: flex;
animation: fadeIn 0.3s ease;
}
Not all CSS properties are created equal when it comes to animation performance. Some properties trigger expensive layout and paint operations.
transform, opacity
color, background-color, box-shadow
width, height, top, left,
margin, padding
/* ❌ Bad - animates layout-triggering properties */
.slow {
transition: width 0.3s, height 0.3s, left 0.3s;
}
/* ✅ Good - uses transform instead */
.fast {
transition: transform 0.3s, opacity 0.3s;
}
/* Use will-change to hint browser optimization */
.animated {
will-change: transform, opacity;
}
/* Remove will-change after animation */
.animated.animation-done {
will-change: auto;
}
/* ❌ Slow - changes layout */
@keyframes slideInSlow {
from { left: -100px; }
to { left: 0; }
}
/* ✅ Fast - uses transform */
@keyframes slideInFast {
from { transform: translateX(-100px); }
to { transform: translateX(0); }
}
/* Reduce or disable animations for users who prefer reduced motion */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
Let's build a complete accordion component using the checkbox hack.
<div class="accordion">
<div class="accordion-item">
<input type="checkbox" id="item1" class="accordion-toggle">
<label for="item1" class="accordion-header">
What is CSS?
<span class="icon">▼</span>
</label>
<div class="accordion-content">
<p>CSS (Cascading Style Sheets) is the language used to style HTML documents.</p>
</div>
</div>
</div>
.accordion-item {
border: 1px solid #e5e7eb;
border-radius: 6px;
margin-bottom: 10px;
overflow: hidden;
}
.accordion-toggle {
display: none;
}
.accordion-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px;
background: #f9fafb;
cursor: pointer;
user-select: none;
transition: background 0.2s;
}
.accordion-header:hover {
background: #f3f4f6;
}
.accordion-icon {
transition: transform 0.3s ease;
}
.accordion-content {
max-height: 0;
overflow: hidden;
transition: max-height 0.4s ease;
}
.accordion-toggle:checked ~ .accordion-header {
background: #eff6ff;
}
.accordion-toggle:checked ~ .accordion-header .icon {
transform: rotate(180deg);
}
.accordion-toggle:checked ~ .accordion-content {
max-height: 500px;
}
Building a modal using the :target pseudo-class.
<a href="#myModal" class="open-modal">Open Modal</a>
<div id="myModal" class="modal">
<div class="modal-content">
<a href="#" class="modal-close">×</a>
<h2>Modal Title</h2>
<p>Modal content goes here.</p>
</div>
</div>
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.8);
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal:target {
display: flex;
animation: fadeIn 0.3s ease;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 8px;
max-width: 500px;
width: 90%;
position: relative;
animation: slideDown 0.3s ease;
}
.modal-close {
position: absolute;
top: 10px;
right: 15px;
font-size: 28px;
color: #666;
text-decoration: none;
transition: color 0.2s;
}
.modal-close:hover {
color: #000;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slideDown {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
Creating a 3D cube with CSS transforms.
<div class="scene">
<div class="cube">
<div class="cube-face front">Front</div>
<div class="cube-face back">Back</div>
<div class="cube-face right">Right</div>
<div class="cube-face left">Left</div>
<div class="cube-face top">Top</div>
<div class="cube-face bottom">Bottom</div>
</div>
</div>
.scene {
width: 200px;
height: 200px;
perspective: 1000px;
}
.cube {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
animation: rotateCube 10s linear infinite;
}
.cube-face {
position: absolute;
width: 200px;
height: 200px;
border: 2px solid white;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
opacity: 0.9;
}
.front { background: rgba(255, 0, 0, 0.7); transform: rotateY(0deg) translateZ(100px); }
.back { background: rgba(0, 255, 0, 0.7); transform: rotateY(180deg) translateZ(100px); }
.right { background: rgba(0, 0, 255, 0.7); transform: rotateY(90deg) translateZ(100px); }
.left { background: rgba(255, 255, 0, 0.7); transform: rotateY(-90deg) translateZ(100px); }
.top { background: rgba(255, 0, 255, 0.7); transform: rotateX(90deg) translateZ(100px); }
.bottom { background: rgba(0, 255, 255, 0.7); transform: rotateX(-90deg) translateZ(100px); }
@keyframes rotateCube {
from { transform: rotateX(0) rotateY(0); }
to { transform: rotateX(360deg) rotateY(360deg); }
}
Creating various loading spinner animations.
/* Simple spinner */
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #3b82f6;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Pulse spinner */
.pulse-spinner {
width: 40px;
height: 40px;
background: #3b82f6;
border-radius: 50%;
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(0.8);
opacity: 1;
}
50% {
transform: scale(1.2);
opacity: 0.5;
}
}
/* Bouncing dots */
.dots-spinner {
display: flex;
gap: 8px;
}
.dot {
width: 12px;
height: 12px;
background: #3b82f6;
border-radius: 50%;
animation: bounce 1.4s ease-in-out infinite;
}
.dot:nth-child(1) { animation-delay: 0s; }
.dot:nth-child(2) { animation-delay: 0.2s; }
.dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes bounce {
0%, 80%, 100% {
transform: translateY(0);
}
40% {
transform: translateY(-20px);
}
}
Challenge: Build an Animated Photo Gallery
Create a photo gallery with the following features:
Bonus Challenges:
In this lesson, you learned:
CSS animations and transforms are powerful tools for creating engaging user experiences. By understanding performance implications and respecting user preferences, you can create delightful interactions that enhance your websites.