CSS Animations
CSS animations allow you to create more complex animations than transitions. With @keyframes, you can define multiple change points (keyframes) along an animation timeline — not just from an initial state to a final state like transitions.
The main differences from transitions:
- Transitions require a trigger (hover, focus, class change)
- Animations can run automatically without a trigger
- Animations support multiple keyframes (not just start-end)
- Animations can repeat (loop), run back and forth (alternate), and be delayed
The main components:
- @keyframes — defines the change points
- animation-name — the name of the keyframes to use
- animation-duration — the duration of one animation cycle
- animation-timing-function — the speed curve
- animation-delay — the delay before the animation starts
- animation-iteration-count — how many times the animation repeats
- animation-direction — the direction of the animation
- animation-fill-mode — the element state before/after the animation
Code Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS Animations</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: #0f0f1a;
color: #fff;
padding: 40px 20px;
}
h1 {
text-align: center;
margin-bottom: 10px;
}
.subtitle {
text-align: center;
color: #888;
margin-bottom: 50px;
}
.section {
max-width: 800px;
margin: 0 auto 60px;
}
.section h2 {
text-align: center;
margin-bottom: 25px;
color: #667eea;
}
.demo-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 30px;
}
.demo-item {
text-align: center;
}
.demo-item h4 {
margin-bottom: 15px;
font-size: 0.8rem;
color: #aaa;
}
/* ===== Loading Spinner ===== */
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
@keyframes pulse-opacity {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.spinner {
width: 50px;
height: 50px;
border: 4px solid rgba(102, 126, 234, 0.2);
border-top-color: #667eea;
border-radius: 50%;
margin: 0 auto;
animation: spin 1s linear infinite;
}
.spinner-dots {
display: flex;
gap: 8px;
justify-content: center;
}
.spinner-dot {
width: 14px;
height: 14px;
background: #667eea;
border-radius: 50%;
animation: pulse-opacity 1.4s ease-in-out infinite;
}
.spinner-dot:nth-child(2) { animation-delay: 0.2s; }
.spinner-dot:nth-child(3) { animation-delay: 0.4s; }
/* ===== Bounce Effect ===== */
@keyframes bounce {
0%, 100% { transform: translateY(0); }
40% { transform: translateY(-30px); }
60% { transform: translateY(-15px); }
}
@keyframes bounce-ball {
0%, 100% {
transform: translateY(0) scaleY(1);
}
30% {
transform: translateY(-40px) scaleY(1);
}
50% {
transform: translateY(0) scaleY(0.8) scaleX(1.2);
}
65% {
transform: translateY(-15px) scaleY(1);
}
80% {
transform: translateY(0) scaleY(0.9) scaleX(1.1);
}
}
.bounce-text {
font-size: 2rem;
font-weight: bold;
color: #f5576c;
display: inline-block;
animation: bounce 2s ease infinite;
}
.bounce-ball {
width: 40px;
height: 40px;
background: #43e97b;
border-radius: 50%;
margin: 0 auto;
animation: bounce-ball 1.5s ease infinite;
}
/* ===== Slide-In ===== */
@keyframes slideInLeft {
from {
transform: translateX(-100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideInRight {
from {
transform: translateX(100%);
opacity: 0;
}
to {
transform: translateX(0);
opacity: 1;
}
}
@keyframes slideInUp {
from {
transform: translateY(50px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.slide-demo {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.slide-box {
padding: 15px 25px;
border-radius: 8px;
font-weight: 600;
font-size: 0.85rem;
}
.slide-left {
background: #667eea;
animation: slideInLeft 0.8s ease-out forwards;
}
.slide-right {
background: #f5576c;
animation: slideInRight 0.8s ease-out 0.2s forwards;
opacity: 0;
}
.slide-up {
background: #43e97b;
color: #1a1a2e;
animation: slideInUp 0.8s ease-out 0.4s forwards;
opacity: 0;
}
/* ===== Pulse ===== */
@keyframes pulse {
0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(102, 126, 234, 0.7); }
70% { transform: scale(1.05); box-shadow: 0 0 0 20px rgba(102, 126, 234, 0); }
100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(102, 126, 234, 0); }
}
.pulse-btn {
width: 120px;
height: 120px;
border-radius: 50%;
background: #667eea;
color: white;
border: none;
font-size: 0.9rem;
font-weight: 600;
cursor: pointer;
animation: pulse 2s ease-in-out infinite;
}
/* ===== Typing Effect ===== */
@keyframes blink-caret {
from, to { border-color: transparent; }
50% { border-color: #667eea; }
}
@keyframes type {
from { width: 0; }
to { width: 100%; }
}
.typing-container {
display: flex;
justify-content: center;
}
.typing-text {
font-family: 'Fira Code', monospace;
font-size: 1.2rem;
color: #667eea;
border-right: 3px solid #667eea;
white-space: nowrap;
overflow: hidden;
width: 0;
animation:
type 3s steps(30, end) forwards,
blink-caret 0.75s step-end infinite;
}
/* ===== Direction Demo ===== */
@keyframes moveRight {
0% { transform: translateX(0); }
100% { transform: translateX(150px); }
}
.direction-track {
width: 200px;
height: 40px;
background: rgba(255,255,255,0.05);
border-radius: 20px;
margin: 0 auto 15px;
position: relative;
overflow: hidden;
}
.direction-ball {
width: 30px;
height: 30px;
background: #667eea;
border-radius: 50%;
position: absolute;
top: 5px;
left: 5px;
}
.dir-normal .direction-ball { animation: moveRight 2s ease infinite normal; }
.dir-reverse .direction-ball { animation: moveRight 2s ease infinite reverse; }
.dir-alternate .direction-ball { animation: moveRight 2s ease infinite alternate; }
.dir-alternate-reverse .direction-ball { animation: moveRight 2s ease infinite alternate-reverse; }
/* ===== Fill Mode Demo ===== */
@keyframes fadeSlide {
from {
transform: translateY(20px);
opacity: 0;
}
to {
transform: translateY(0);
opacity: 1;
}
}
.fill-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 20px;
}
.fill-item {
background: rgba(102, 126, 234, 0.15);
border: 1px solid rgba(102, 126, 234, 0.3);
border-radius: 8px;
padding: 20px;
text-align: center;
}
.fill-item h4 {
font-size: 0.75rem;
color: #667eea;
margin-bottom: 10px;
}
.fill-none {
animation: fadeSlide 0.5s ease none;
}
.fill-forwards {
animation: fadeSlide 0.5s ease forwards;
}
.fill-backwards {
animation: fadeSlide 0.5s ease backwards;
}
.fill-both {
animation: fadeSlide 0.5s ease both;
}
.fill-item p {
font-size: 0.8rem;
color: #ccc;
}
/* ===== Practical Examples ===== */
@keyframes gradientShift {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-5px); }
20%, 40%, 60%, 80% { transform: translateX(5px); }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-20px); }
}
@keyframes ripple {
0% { transform: scale(0); opacity: 1; }
100% { transform: scale(4); opacity: 0; }
}
.practical-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 30px;
}
.practical-item {
text-align: center;
}
.practical-item h4 {
margin-bottom: 15px;
font-size: 0.8rem;
color: #aaa;
}
.gradient-box {
width: 150px;
height: 150px;
border-radius: 16px;
background: linear-gradient(-45deg, #667eea, #764ba2, #f5576c, #4facfe);
background-size: 400% 400%;
animation: gradientShift 4s ease infinite;
margin: 0 auto;
}
.shake-text {
font-size: 1.5rem;
font-weight: bold;
color: #f5576c;
display: inline-block;
cursor: pointer;
}
.shake-text:hover {
animation: shake 0.5s ease;
}
.float-element {
font-size: 3rem;
animation: float 3s ease-in-out infinite;
}
.ripple-btn {
width: 120px;
height: 50px;
background: #667eea;
color: white;
border: none;
border-radius: 8px;
font-weight: 600;
cursor: pointer;
position: relative;
overflow: hidden;
margin: 0 auto;
}
.ripple-btn::after {
content: '';
position: absolute;
width: 50px;
height: 50px;
background: rgba(255,255,255,0.3);
border-radius: 50%;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0);
opacity: 0;
}
.ripple-btn:active::after {
animation: ripple 0.6s ease-out;
}
/* ===== Animation Properties Table ===== */
.props-table {
width: 100%;
border-collapse: collapse;
margin-top: 20px;
}
.props-table th,
.props-table td {
padding: 12px 16px;
text-align: left;
border-bottom: 1px solid rgba(255,255,255,0.1);
font-size: 0.85rem;
}
.props-table th {
color: #667eea;
font-weight: 600;
}
.props-table td:first-child {
font-family: monospace;
color: #43e97b;
}
.code-example {
background: #1a1a2e;
border-radius: 12px;
padding: 25px;
margin-top: 30px;
font-family: monospace;
font-size: 0.85rem;
line-height: 1.6;
color: #e0e0e0;
overflow-x: auto;
}
.code-example .keyword { color: #c792ea; }
.code-example .property { color: #82aaff; }
.code-example .value { color: #c3e88d; }
.code-example .comment { color: #546e7a; }
</style>
</head>
<body>
<h1>CSS Animations</h1>
<p class="subtitle">Automatic animations without JavaScript using @keyframes</p>
<div class="section">
<h2>Loading Spinner</h2>
<div class="demo-grid">
<div class="demo-item">
<h4>Rotating Spinner</h4>
<div class="spinner"></div>
</div>
<div class="demo-item">
<h4>Pulsing Dots</h4>
<div class="spinner-dots">
<div class="spinner-dot"></div>
<div class="spinner-dot"></div>
<div class="spinner-dot"></div>
</div>
</div>
</div>
</div>
<div class="section">
<h2>Bounce Effect</h2>
<div class="demo-grid">
<div class="demo-item">
<h4>Bouncing Text</h4>
<div class="bounce-text">HOI!</div>
</div>
<div class="demo-item">
<h4>Bouncing Ball</h4>
<div class="bounce-ball"></div>
</div>
</div>
</div>
<div class="section">
<h2>Slide-In Animation</h2>
<div class="slide-demo">
<div class="slide-box slide-left">← Left</div>
<div class="slide-box slide-right">Right →</div>
<div class="slide-box slide-up">↑ Up</div>
</div>
</div>
<div class="section">
<h2>Pulse Effect</h2>
<div class="demo-item">
<h4>Click the Button</h4>
<button class="pulse-btn">Pulse</button>
</div>
</div>
<div class="section">
<h2>Typing Effect</h2>
<div class="typing-container">
<div class="typing-text">Welcome to CSS Animations!</div>
</div>
</div>
<div class="section">
<h2>Animation Direction</h2>
<div class="demo-grid">
<div class="demo-item">
<h4>normal</h4>
<div class="direction-track dir-normal">
<div class="direction-ball"></div>
</div>
</div>
<div class="demo-item">
<h4>reverse</h4>
<div class="direction-track dir-reverse">
<div class="direction-ball"></div>
</div>
</div>
<div class="demo-item">
<h4>alternate</h4>
<div class="direction-track dir-alternate">
<div class="direction-ball"></div>
</div>
</div>
<div class="demo-item">
<h4>alternate-reverse</h4>
<div class="direction-track dir-alternate-reverse">
<div class="direction-ball"></div>
</div>
</div>
</div>
</div>
<div class="section">
<h2>Animation Fill Mode</h2>
<div class="fill-grid">
<div class="fill-item">
<h4>none</h4>
<div class="fill-none" >Text</div>
<p>Reset to the initial state after finishing</p>
</div>
<div class="fill-item">
<h4>forwards</h4>
<div class="fill-forwards" >Text</div>
<p>Retain the final state of the animation</p>
</div>
<div class="fill-item">
<h4>backwards</h4>
<div class="fill-backwards" >Text</div>
<p>Apply the initial state during the delay</p>
</div>
<div class="fill-item">
<h4>both</h4>
<div class="fill-both" >Text</div>
<p>Kombinasi forwards + backwards</p>
</div>
</div>
</div>
<div class="section">
<h2>Everyday Practice</h2>
<div class="practical-grid">
<div class="practical-item">
<h4>Gradient Background Shift</h4>
<div class="gradient-box"></div>
</div>
<div class="practical-item">
<h4>Error Shake</h4>
<div class="shake-text">EROOR!</div>
<p style="font-size:0.8rem;color:#888;margin-top:10px;">hover for the shake effect</p>
</div>
<div class="practical-item">
<h4>Floating Element</h4>
<div class="float-element">🎈</div>
</div>
<div class="practical-item">
<h4>Ripple Button</h4>
<button class="ripple-btn">Click Me</button>
<p style="font-size:0.8rem;color:#888;margin-top:10px;">click for the ripple effect</p>
</div>
</div>
</div>
<div class="section">
<h2>Referensi Properti</h2>
<table class="props-table">
<thead>
<tr>
<th>Properti</th>
<th>Default Value</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td>animation-name</td>
<td>none</td>
<td>The name of the keyframes to use</td>
</tr>
<tr>
<td>animation-duration</td>
<td>0s</td>
<td>Duration of one animation cycle</td>
</tr>
<tr>
<td>animation-timing-function</td>
<td>ease</td>
<td>Animation speed curve</td>
</tr>
<tr>
<td>animation-delay</td>
<td>0s</td>
<td>Delay time before the animation starts</td>
</tr>
<tr>
<td>animation-iteration-count</td>
<td>1</td>
<td>Number of repetitions (a number or infinite)</td>
</tr>
<tr>
<td>animation-direction</td>
<td>normal</td>
<td>Direction: normal, reverse, alternate, alternate-reverse</td>
</tr>
<tr>
<td>animation-fill-mode</td>
<td>none</td>
<td>State before/after: none, forwards, backwards, both</td>
</tr>
<tr>
<td>animation-play-state</td>
<td>running</td>
<td>running or paused</td>
</tr>
</tbody>
</table>
</div>
<div class="code-example">
<span class="comment">/* Shorthand: name duration timing delay count direction fill */</span><br>
<span class="property">animation</span>: <span class="value">fadeIn 0.5s ease 0.2s 1 normal forwards</span>;<br><br>
<span class="comment">/* Example: loading spinner */</span><br>
<span class="keyword">@keyframes</span> <span class="value">spin</span> {<br>
<span class="property">from</span> { <span class="property">transform</span>: <span class="value">rotate(0deg)</span>; }<br>
<span class="property">to</span> { <span class="property">transform</span>: <span class="value">rotate(360deg)</span>; }<br>
}<br><br>
<span class="property">.spinner</span> {<br>
<span class="property">animation</span>: <span class="value">spin 1s linear infinite</span>;<br>
}
</div>
</body>
</html>
Code Explanation
@keyframes
@keyframes defines change points along the animation timeline. You can use from/to (equivalent to 0%/100%) or specific percentages for multiple points.
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes colorShift {
0% { background: red; }
33% { background: yellow; }
66% { background: green; }
100% { background: red; }
}
animation-name
Connects an element to keyframes that have been defined.
.box {
animation-name: fadeIn;
}
animation-duration
Determines how long one animation cycle lasts. It must be greater than 0.
.box {
animation-duration: 2s; /* 2 seconds per cycle */
animation-duration: 500ms; /* 500 milliseconds */
}
animation-delay
The delay before the animation starts. It can be negative to "start" the animation partway through.
.box {
animation-delay: 0.5s; /* wait 0.5 seconds */
animation-delay: -1s; /* start from the 1st second */
}
animation-iteration-count
How many times the animation repeats. It can be a number or infinite.
.box {
animation-iteration-count: 3; /* repeat 3 times */
animation-iteration-count: infinite; /* repeat continuously */
}
animation-direction
Controls the direction of the animation:
- normal — 0% to 100%, then restart from 0%
- reverse — 100% to 0%, then restart from 100%
- alternate — 0% to 100%, then 100% back to 0% (back and forth)
- alternate-reverse — the reverse of alternate
.box {
animation-direction: alternate; /* back and forth */
}
animation-fill-mode
Controls the element state before and after the animation:
- none — the element returns to its original CSS state
- forwards — keep the final state of the animation
- backwards — apply the initial state during the delay
- both — a combination of forwards + backwards
.box {
animation-fill-mode: forwards; /* stay in the final state */
}
animation Shorthand
/* animation: name duration timing-function delay iteration-count direction fill-mode play-state; */
.box {
animation: fadeIn 0.5s ease 0s 1 normal forwards;
}
/* Can have multiple animations */
.box {
animation: fadeIn 0.5s ease, slideUp 0.8s ease 0.3s;
}
Percentages in @keyframes
You can use percentages to control the timing of changes with more precision:
@keyframes complex {
0% { transform: translateX(0); opacity: 0; }
20% { transform: translateX(50px); opacity: 1; }
50% { transform: translateX(100px); }
80% { transform: translateX(50px); opacity: 0.5; }
100% { transform: translateX(0); opacity: 1; }
}
Everyday Use Cases
1. Loading Spinner on a Website
Every modern website uses a spinner while content loads. CSS animation enables continuous rotation without JavaScript:
@keyframes spin {
to { transform: rotate(360deg); }
}
.loader {
width: 40px;
height: 40px;
border: 3px solid #eee;
border-top-color: #667eea;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
2. Error Shake on a Form
When a user fills out a form incorrectly, the element shakes to indicate the error:
@keyframes shake {
0%, 100% { transform: translateX(0); }
20%, 60% { transform: translateX(-8px); }
40%, 80% { transform: translateX(8px); }
}
.input-error {
border-color: red;
animation: shake 0.4s ease;
}
3. Hero Section Background
A slowly moving gradient background in the hero section gives a modern and dynamic impression:
@keyframes gradientMove {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.hero {
background: linear-gradient(-45deg, #667eea, #764ba2, #f5576c, #4facfe);
background-size: 400% 400%;
animation: gradientMove 10s ease infinite;
}
4. Notification Badge Pulse
A notification badge that pulses to catch the user's attention:
@keyframes badgePulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.2); }
}
.notification-badge {
animation: badgePulse 2s ease-in-out infinite;
}
5. Page Transition
A fade-in animation when a new page loads — providing a smooth transition between pages:
@keyframes pageIn {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.page-content {
animation: pageIn 0.6s ease forwards;
}
Conclusion
CSS animations give you full control over complex animations through @keyframes and the various animation properties. @keyframes defines the change points, animation-iteration-count: infinite makes an animation repeat forever, animation-direction: alternate makes it run back and forth, and animation-fill-mode: forwards keeps the final state. CSS animations are more powerful than transitions because they support multiple keyframes, run automatically without a trigger, and can loop indefinitely. Use transitions for simple state-based animations, and animations for complex ones that run automatically.