🚀 CSS Animation
CSS Animations allow you to create smooth, controllable animations without JavaScript using keyframes and animation properties.
✏️ Basic Structure of CSS Animation
@keyframes animation-name {
from {
/* starting state */
}
to {
/* ending state */
}
}Apply the animation to an element:
<body>
<div class="element">Animated!</div>
</body><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>CSS Animation: Keyframes</title>
<style>
.infographic-anim {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
box-sizing: border-box;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--paper);
color: var(--ink);
border-radius: 14px;
overflow: hidden;
}
.infographic-anim[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
}
.an-inner { padding: 0.5rem; }
.an-layout { display: flex; flex-direction: column; gap: 1.2rem; }
.an-typebar { display: flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; align-self: flex-start; }
.an-type-btn { border: none; background: var(--paper); color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.75rem; font-weight: 700; padding: 0.4rem 0.9rem; cursor: pointer; }
.an-type-btn + .an-type-btn { border-left: 1px solid var(--line); }
.an-type-btn.an-type-on { background: var(--accent); color: #fff; }
.an-code-wrap { background: #0b0b0b; border: 1px solid #2a2a2a; border-radius: 10px; overflow: hidden; }
.an-filetabs { display: flex; background: #161616; border-bottom: 1px solid #2a2a2a; }
.an-filetab { border: none; background: transparent; color: #fff; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.78rem; font-weight: 700; padding: 0.6rem 1rem; border-bottom: 2px solid #5b8def; }
.an-code { padding: 1rem 1.8rem 1rem 0.9rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.76rem; line-height: 1.8; overflow-x: auto; }
.an-line { display: block; white-space: pre; color: #e5e5e5; padding: 0.05rem 0.4rem; border-radius: 5px; }
.an-line.an-hl { background: #1c2333; box-shadow: inset 3px 0 0 #5b8def; color: #ffffff; }
.an-sel { color: #f472b6; font-weight: 700; }
.an-prop { color: #c586c0; }
.an-val { color: #ce9178; }
.an-cmt { color: #6a737d; }
.an-at { color: #60a5fa; font-weight: 700; }
.an-browser { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--paper); }
.an-browser-bar { background: var(--paper-alt); padding: 0.5rem 0.7rem; display: flex; align-items: center; gap: 0.5rem; border-bottom: 1px solid var(--line); }
.an-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--line); }
.an-tab { background: var(--paper); border: 1px solid var(--line); border-bottom: none; border-radius: 6px 6px 0 0; font-size: 0.72rem; color: var(--ink-soft); padding: 0.25rem 0.6rem; }
.an-screen {
padding: 1.8rem 1rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
background: var(--paper-alt);
min-height: 170px;
justify-content: center;
}
.an-element {
width: 170px; height: 85px;
background-color: #e74c3c;
color: white;
font-size: 1.05rem;
display: flex; align-items: center; justify-content: center;
border-radius: 8px;
box-sizing: border-box;
opacity: 0;
transform: translateY(-50px);
}
.an-element.an-playing {
animation-name: an-slideFade;
animation-fill-mode: forwards;
}
@keyframes an-slideFade {
0% { opacity: 0; transform: translateY(-50px); }
100% { opacity: 1; transform: translateY(0); }
}
.an-timeline {
position: relative;
width: 220px;
height: 4px;
background: var(--line);
border-radius: 2px;
}
.an-timeline-fill {
position: absolute;
left: 0; top: 0; height: 100%;
background: var(--accent);
border-radius: 2px;
width: 0%;
}
.an-timeline-marks {
display: flex; justify-content: space-between;
width: 220px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 0.65rem;
color: var(--ink-soft);
}
.an-controls { background: var(--paper-alt); padding: 0.8rem 0.9rem 1rem; border-top: 1px solid var(--line); }
.an-control-group { display: none; flex-direction: column; gap: 0.7rem; }
.an-control-group.an-group-on { display: flex; }
.an-control-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.an-control-label { font-size: 0.72rem; color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; min-width: 70px; }
.an-range { flex: 1; min-width: 100px; accent-color: var(--accent); cursor: pointer; }
.an-readout { font-size: 0.78rem; color: var(--ink); font-weight: 700; font-family: "SFMono-Regular", Consolas, monospace; min-width: 55px; text-align: right; }
.an-replay-btn {
border: 1px solid var(--accent);
background: var(--accent);
color: #fff;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 0.75rem;
font-weight: 700;
padding: 0.45rem 1rem;
border-radius: 999px;
cursor: pointer;
}
.an-segmented { display: flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; flex-wrap: wrap; }
.an-seg-btn { border: none; background: var(--paper); color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.68rem; font-weight: 700; padding: 0.32rem 0.6rem; cursor: pointer; white-space: nowrap; }
.an-seg-btn + .an-seg-btn { border-left: 1px solid var(--line); }
.an-seg-btn.an-seg-on { background: var(--accent); color: #fff; }
.an-note { font-size: 0.82rem; color: var(--ink-soft); padding: 0 0.2rem; }
.an-note strong { color: var(--ink); }
.an-note code { font-family: "SFMono-Regular", Consolas, monospace; background: var(--paper-alt); padding: 0.05rem 0.3rem; border-radius: 4px; }
</style>
</head>
<body>
<div class="infographic-anim" id="an-root">
<div class="an-inner">
<div class="an-layout">
<div class="an-typebar" id="an-typebar">
<button class="an-type-btn an-type-on" data-tab="replay">Replay</button>
<button class="an-type-btn" data-tab="scrub">Scrub keyframes</button>
</div>
<div class="an-code-wrap">
<div class="an-filetabs">
<button class="an-filetab">styles.css</button>
</div>
<div class="an-code">
<span class="an-line"><span class="an-sel">.element</span> {</span>
<span class="an-line"> <span class="an-prop">width</span>: <span class="an-val">200px</span>;</span>
<span class="an-line"> <span class="an-prop">height</span>: <span class="an-val">100px</span>;</span>
<span class="an-line"> <span class="an-prop">background-color</span>: <span class="an-val">#e74c3c</span>;</span>
<span class="an-line"></span>
<span class="an-line" id="an-line-name"> <span class="an-prop">animation-name</span>: <span class="an-val">slideFade</span>;</span>
<span class="an-line" id="an-line-duration"> <span class="an-prop">animation-duration</span>: <span class="an-val" id="an-val-duration">2s</span>;</span>
<span class="an-line" id="an-line-timing"> <span class="an-prop">animation-timing-function</span>: <span class="an-val" id="an-val-timing">ease</span>;</span>
<span class="an-line"> <span class="an-prop">animation-fill-mode</span>: <span class="an-val">forwards</span>; <span class="an-cmt">/* keeps final state */</span></span>
<span class="an-line">}</span>
<span class="an-line"></span>
<span class="an-line"><span class="an-at">@keyframes</span> <span class="an-val">slideFade</span> {</span>
<span class="an-line" id="an-line-0"> <span class="an-val">0%</span> {</span>
<span class="an-line"> <span class="an-prop">opacity</span>: <span class="an-val">0</span>;</span>
<span class="an-line"> <span class="an-prop">transform</span>: <span class="an-val">translateY(-50px)</span>;</span>
<span class="an-line"> }</span>
<span class="an-line" id="an-line-100"> <span class="an-val">100%</span> {</span>
<span class="an-line"> <span class="an-prop">opacity</span>: <span class="an-val">1</span>;</span>
<span class="an-line"> <span class="an-prop">transform</span>: <span class="an-val">translateY(0)</span>;</span>
<span class="an-line"> }</span>
<span class="an-line">}</span>
</div>
</div>
<div class="an-browser">
<div class="an-browser-bar">
<span class="an-dot"></span><span class="an-dot"></span><span class="an-dot"></span>
<div class="an-tab">🌐 Animation</div>
</div>
<div class="an-screen">
<div class="an-element" id="an-element">Animated!</div>
<div class="an-timeline">
<div class="an-timeline-fill" id="an-timeline-fill"></div>
</div>
<div class="an-timeline-marks"><span>0%</span><span>50%</span><span>100%</span></div>
</div>
<div class="an-controls">
<div class="an-control-group an-group-on" id="an-group-replay">
<div class="an-control-row">
<button class="an-replay-btn" id="an-replay">▶ Replay</button>
<span class="an-control-label" style="min-width:auto;">duration</span>
<input type="range" class="an-range" id="an-duration" min="0.3" max="4" step="0.1" value="2">
<span class="an-readout" id="an-duration-readout">2s</span>
</div>
<div class="an-control-row">
<span class="an-control-label">timing</span>
<div class="an-segmented" id="an-timing-seg">
<button class="an-seg-btn an-seg-on" data-timing="ease">ease</button>
<button class="an-seg-btn" data-timing="linear">linear</button>
<button class="an-seg-btn" data-timing="ease-in">ease-in</button>
<button class="an-seg-btn" data-timing="cubic-bezier(.34,1.56,.64,1)">bouncy</button>
</div>
</div>
</div>
<div class="an-control-group" id="an-group-scrub">
<div class="an-control-row">
<span class="an-control-label">progress</span>
<input type="range" class="an-range" id="an-scrub" min="0" max="100" step="1" value="0">
<span class="an-readout" id="an-scrub-readout">0%</span>
</div>
</div>
</div>
</div>
<div class="an-note" id="an-note">
<strong>animation-fill-mode: forwards</strong> is why the box stays visible after finishing — without it, the element would snap back to its pre-animation CSS the instant the animation ends. Hit Replay to run it again from 0%.
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-anim')
|| document.querySelector('.infographic-anim');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
})();
(function () {
const typeButtons = document.querySelectorAll('.an-type-btn');
const groups = {
replay: document.getElementById('an-group-replay'),
scrub: document.getElementById('an-group-scrub')
};
const el = document.getElementById('an-element');
const note = document.getElementById('an-note');
const timelineFill = document.getElementById('an-timeline-fill');
const replayBtn = document.getElementById('an-replay');
const durationRange = document.getElementById('an-duration');
const durationReadout = document.getElementById('an-duration-readout');
const valDuration = document.getElementById('an-val-duration');
const valTiming = document.getElementById('an-val-timing');
const timingSeg = document.querySelectorAll('#an-timing-seg .an-seg-btn');
const lineDuration = document.getElementById('an-line-duration');
const lineTiming = document.getElementById('an-line-timing');
const line0 = document.getElementById('an-line-0');
const line100 = document.getElementById('an-line-100');
const scrubRange = document.getElementById('an-scrub');
const scrubReadout = document.getElementById('an-scrub-readout');
let duration = 2;
let timing = 'ease';
let mode = 'replay';
let animTimer = null;
let animStart = null;
function switchTab(tab) {
mode = tab;
typeButtons.forEach(b => b.classList.toggle('an-type-on', b.getAttribute('data-tab') === tab));
Object.entries(groups).forEach(([key, g]) => g.classList.toggle('an-group-on', key === tab));
if (tab === 'scrub') {
el.classList.remove('an-playing');
applyScrub(Number(scrubRange.value));
note.innerHTML = '<strong>Drag the progress slider</strong> to move through the keyframe timeline manually — this is exactly what the browser interpolates automatically between <code>0%</code> and <code>100%</code> once you hit Replay.';
} else {
resetToStart();
note.innerHTML = '<strong>animation-fill-mode: forwards</strong> is why the box stays visible after finishing — without it, the element would snap back to its pre-animation CSS the instant the animation ends. Hit Replay to run it again from 0%.';
}
}
function applyScrub(pct) {
const t = pct / 100;
el.style.opacity = t;
el.style.transform = `translateY(${-50 + 50 * t}px)`;
timelineFill.style.width = pct + '%';
scrubReadout.textContent = pct + '%';
line0.classList.toggle('an-hl', pct < 50);
line100.classList.toggle('an-hl', pct >= 50);
}
function resetToStart() {
el.classList.remove('an-playing');
el.style.opacity = '';
el.style.transform = '';
timelineFill.style.width = '0%';
}
function playReplay() {
if (animTimer) { clearInterval(animTimer); animTimer = null; }
el.classList.remove('an-playing');
el.style.opacity = '';
el.style.transform = '';
el.style.animationDuration = duration + 's';
el.style.animationTimingFunction = timing;
void el.offsetWidth;
el.classList.add('an-playing');
timelineFill.style.width = '0%';
animStart = performance.now();
animTimer = setInterval(() => {
const elapsed = (performance.now() - animStart) / 1000;
const pct = Math.min(100, (elapsed / duration) * 100);
timelineFill.style.width = pct + '%';
line0.classList.toggle('an-hl', pct < 50);
line100.classList.toggle('an-hl', pct >= 50);
if (pct >= 100) { clearInterval(animTimer); animTimer = null; }
}, 30);
}
function render() {
valDuration.textContent = duration + 's';
valTiming.textContent = timing;
durationReadout.textContent = duration + 's';
}
typeButtons.forEach(btn => btn.addEventListener('click', () => switchTab(btn.getAttribute('data-tab'))));
replayBtn.addEventListener('click', () => {
lineDuration.classList.remove('an-hl');
lineTiming.classList.remove('an-hl');
playReplay();
});
durationRange.addEventListener('input', () => {
duration = Number(durationRange.value);
render();
lineDuration.classList.add('an-hl');
});
timingSeg.forEach(btn => {
btn.addEventListener('click', () => {
timing = btn.getAttribute('data-timing');
timingSeg.forEach(b => b.classList.toggle('an-seg-on', b === btn));
render();
lineTiming.classList.add('an-hl');
});
});
scrubRange.addEventListener('input', () => applyScrub(Number(scrubRange.value)));
render();
resetToStart();
setTimeout(playReplay, 400);
})();
</script>
</body>
</html>📌 Summary
@keyframesdefines the animation sequence.Use
animation-name,animation-duration, and other properties to control how the animation runs.animation-fill-mode: forwardsensures the element stays in the final state after the animation ends.
💡Essential Animation Properties
Feature | Example | Description |
|---|---|---|
@keyframes |
| Defines animation steps |
animation-name |
| Links keyframes |
animation-duration |
| Duration of animation |
animation-iteration-count |
| Repeats or runs once |
animation-direction |
| Changes direction |
animation-fill-mode |
| Retains styles after animation ends |
❗️Shorthand Syntax
Instead of writing all animation properties separately, use shorthand:
animation: animation-name duration timing-function delay iteration-count direction fill-mode;Example:
<body>
<div class="box">Hello!</div>
</body><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Animation Shorthand</title>
<style>
.infographic-shorthand {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
box-sizing: border-box;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--paper);
color: var(--ink);
border-radius: 14px;
overflow: hidden;
}
.infographic-shorthand[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
}
.sh-inner { padding: 0.5rem; }
.sh-layout { display: flex; flex-direction: column; gap: 1.2rem; }
.sh-typebar { display: flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; align-self: flex-start; flex-wrap: wrap; }
.sh-type-btn { border: none; background: var(--paper); color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.72rem; font-weight: 700; padding: 0.4rem 0.8rem; cursor: pointer; white-space: nowrap; }
.sh-type-btn + .sh-type-btn { border-left: 1px solid var(--line); }
.sh-type-btn.sh-type-on { background: var(--accent); color: #fff; }
.sh-code-wrap { background: #0b0b0b; border: 1px solid #2a2a2a; border-radius: 10px; overflow: hidden; }
.sh-filetabs { display: flex; background: #161616; border-bottom: 1px solid #2a2a2a; }
.sh-filetab { border: none; background: transparent; color: #fff; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.78rem; font-weight: 700; padding: 0.6rem 1rem; border-bottom: 2px solid #5b8def; }
.sh-code { padding: 1rem 1.4rem 1rem 0.9rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.74rem; line-height: 1.9; overflow-x: auto; }
.sh-line { display: block; white-space: pre-wrap; color: #e5e5e5; padding: 0.05rem 0.4rem; border-radius: 5px; }
.sh-sel { color: #f472b6; font-weight: 700; }
.sh-prop { color: #c586c0; }
.sh-cmt { color: #6a737d; }
.sh-part { opacity: 0.32; transition: opacity 0.15s ease, box-shadow 0.15s ease; border-radius: 3px; padding: 0 1px; }
.sh-part.sh-part-on { opacity: 1; background: #1c2333; box-shadow: inset 0 0 0 1px #5b8def; }
.sh-browser { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--paper); }
.sh-browser-bar { background: var(--paper-alt); padding: 0.5rem 0.7rem; display: flex; align-items: center; gap: 0.5rem; border-bottom: 1px solid var(--line); }
.sh-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--line); }
.sh-tab { background: var(--paper); border: 1px solid var(--line); border-bottom: none; border-radius: 6px 6px 0 0; font-size: 0.72rem; color: var(--ink-soft); padding: 0.25rem 0.6rem; }
.sh-screen {
padding: 1.8rem 1rem 1.2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.8rem;
background: var(--paper-alt);
min-height: 160px;
justify-content: center;
}
.sh-box {
width: 170px; height: 85px;
background-color: #2ecc71;
color: white;
font-size: 1rem;
display: flex; align-items: center; justify-content: center;
border-radius: 10px;
box-sizing: border-box;
opacity: 0;
}
.sh-box.sh-playing { animation-name: sh-fadeIn; }
@keyframes sh-fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.sh-status { font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.7rem; color: var(--ink-soft); }
.sh-status span { color: var(--accent); font-weight: 700; }
.sh-controls { background: var(--paper-alt); padding: 0.8rem 0.9rem 1rem; border-top: 1px solid var(--line); }
.sh-control-group { display: none; flex-direction: column; gap: 0.7rem; }
.sh-control-group.sh-group-on { display: flex; }
.sh-control-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.sh-control-label { font-size: 0.72rem; color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; min-width: 70px; }
.sh-range { flex: 1; min-width: 100px; accent-color: var(--accent); cursor: pointer; }
.sh-readout { font-size: 0.78rem; color: var(--ink); font-weight: 700; font-family: "SFMono-Regular", Consolas, monospace; min-width: 45px; text-align: right; }
.sh-segmented { display: flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; flex-wrap: wrap; }
.sh-seg-btn { border: none; background: var(--paper); color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.66rem; font-weight: 700; padding: 0.3rem 0.55rem; cursor: pointer; white-space: nowrap; }
.sh-seg-btn + .sh-seg-btn { border-left: 1px solid var(--line); }
.sh-seg-btn.sh-seg-on { background: var(--accent); color: #fff; }
.sh-replay-row { display: flex; align-items: center; gap: 0.7rem; margin-bottom: 0.2rem; }
.sh-replay-btn { border: 1px solid var(--accent); background: var(--accent); color: #fff; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.75rem; font-weight: 700; padding: 0.45rem 1rem; border-radius: 999px; cursor: pointer; }
.sh-note { font-size: 0.82rem; color: var(--ink-soft); padding: 0 0.2rem; }
.sh-note strong { color: var(--ink); }
.sh-note code { font-family: "SFMono-Regular", Consolas, monospace; background: var(--paper-alt); padding: 0.05rem 0.3rem; border-radius: 4px; }
</style>
</head>
<body>
<div class="infographic-shorthand" id="sh-root">
<div class="sh-inner">
<div class="sh-layout">
<div class="sh-typebar" id="sh-typebar">
<button class="sh-type-btn sh-type-on" data-tab="duration">Duration/delay</button>
<button class="sh-type-btn" data-tab="timing">Timing fn</button>
<button class="sh-type-btn" data-tab="repeat">Repeat/direction</button>
<button class="sh-type-btn" data-tab="fill">Fill mode</button>
</div>
<div class="sh-code-wrap">
<div class="sh-filetabs">
<button class="sh-filetab">styles.css</button>
</div>
<div class="sh-code">
<span class="sh-line"><span class="sh-cmt">@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }</span></span>
<span class="sh-line"></span>
<span class="sh-line"><span class="sh-sel">.box</span> {</span>
<span class="sh-line"> <span class="sh-prop">animation</span>:
<span class="sh-part" id="sh-part-name">fadeIn</span>
<span class="sh-part" id="sh-part-duration">2s</span>
<span class="sh-part" id="sh-part-timing">ease-in</span>
<span class="sh-part" id="sh-part-delay">0s</span>
<span class="sh-part" id="sh-part-count">1</span>
<span class="sh-part" id="sh-part-direction">normal</span>
<span class="sh-part" id="sh-part-fill">forwards</span>;</span>
<span class="sh-line">}</span>
<span class="sh-line"></span>
<span class="sh-line"><span class="sh-cmt">/* name duration timing-fn delay count direction fill-mode */</span></span>
</div>
</div>
<div class="sh-browser">
<div class="sh-browser-bar">
<span class="sh-dot"></span><span class="sh-dot"></span><span class="sh-dot"></span>
<div class="sh-tab">🌐 Fade In</div>
</div>
<div class="sh-screen">
<div class="sh-box" id="sh-box">Hello!</div>
<div class="sh-status" id="sh-status">state: <span>idle</span></div>
</div>
<div class="sh-controls">
<div class="sh-replay-row">
<button class="sh-replay-btn" id="sh-replay">▶ Replay</button>
</div>
<div class="sh-control-group sh-group-on" id="sh-group-duration">
<div class="sh-control-row">
<span class="sh-control-label">duration</span>
<input type="range" class="sh-range" id="sh-duration" min="0.3" max="4" step="0.1" value="2">
<span class="sh-readout" id="sh-duration-readout">2s</span>
</div>
<div class="sh-control-row">
<span class="sh-control-label">delay</span>
<input type="range" class="sh-range" id="sh-delay" min="0" max="2" step="0.1" value="0">
<span class="sh-readout" id="sh-delay-readout">0s</span>
</div>
</div>
<div class="sh-control-group" id="sh-group-timing">
<div class="sh-control-row">
<span class="sh-control-label">timing-fn</span>
<div class="sh-segmented" id="sh-timing-seg">
<button class="sh-seg-btn sh-seg-on" data-timing="ease-in">ease-in</button>
<button class="sh-seg-btn" data-timing="ease">ease</button>
<button class="sh-seg-btn" data-timing="linear">linear</button>
<button class="sh-seg-btn" data-timing="ease-out">ease-out</button>
</div>
</div>
</div>
<div class="sh-control-group" id="sh-group-repeat">
<div class="sh-control-row">
<span class="sh-control-label">count</span>
<div class="sh-segmented" id="sh-count-seg">
<button class="sh-seg-btn sh-seg-on" data-count="1">1</button>
<button class="sh-seg-btn" data-count="2">2</button>
<button class="sh-seg-btn" data-count="3">3</button>
<button class="sh-seg-btn" data-count="infinite">infinite</button>
</div>
</div>
<div class="sh-control-row">
<span class="sh-control-label">direction</span>
<div class="sh-segmented" id="sh-direction-seg">
<button class="sh-seg-btn sh-seg-on" data-direction="normal">normal</button>
<button class="sh-seg-btn" data-direction="reverse">reverse</button>
<button class="sh-seg-btn" data-direction="alternate">alternate</button>
</div>
</div>
</div>
<div class="sh-control-group" id="sh-group-fill">
<div class="sh-control-row">
<span class="sh-control-label">fill-mode</span>
<div class="sh-segmented" id="sh-fill-seg">
<button class="sh-seg-btn" data-fill="none">none</button>
<button class="sh-seg-btn sh-seg-on" data-fill="forwards">forwards</button>
<button class="sh-seg-btn" data-fill="backwards">backwards</button>
<button class="sh-seg-btn" data-fill="both">both</button>
</div>
</div>
</div>
</div>
</div>
<div class="sh-note" id="sh-note">
<strong>Seven values, one property.</strong> <code>animation</code> is shorthand for <code>animation-name</code>, <code>-duration</code>, <code>-timing-function</code>, <code>-delay</code>, <code>-iteration-count</code>, <code>-direction</code>, and <code>-fill-mode</code> — always roughly in that order. Adjust duration and delay, then hit Replay.
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-shorthand')
|| document.querySelector('.infographic-shorthand');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
})();
(function () {
const typeButtons = document.querySelectorAll('.sh-type-btn');
const groups = {
duration: document.getElementById('sh-group-duration'),
timing: document.getElementById('sh-group-timing'),
repeat: document.getElementById('sh-group-repeat'),
fill: document.getElementById('sh-group-fill')
};
const parts = {
duration: [document.getElementById('sh-part-duration')],
timing: [document.getElementById('sh-part-timing')],
repeat: [document.getElementById('sh-part-count'), document.getElementById('sh-part-direction')],
fill: [document.getElementById('sh-part-fill')]
};
const allParts = document.querySelectorAll('.sh-part');
const box = document.getElementById('sh-box');
const statusText = document.querySelector('#sh-status span');
const note = document.getElementById('sh-note');
const replayBtn = document.getElementById('sh-replay');
const durationRange = document.getElementById('sh-duration');
const durationReadout = document.getElementById('sh-duration-readout');
const delayRange = document.getElementById('sh-delay');
const delayReadout = document.getElementById('sh-delay-readout');
const timingSeg = document.querySelectorAll('#sh-timing-seg .sh-seg-btn');
const countSeg = document.querySelectorAll('#sh-count-seg .sh-seg-btn');
const directionSeg = document.querySelectorAll('#sh-direction-seg .sh-seg-btn');
const fillSeg = document.querySelectorAll('#sh-fill-seg .sh-seg-btn');
const partDuration = document.getElementById('sh-part-duration');
const partTiming = document.getElementById('sh-part-timing');
const partDelay = document.getElementById('sh-part-delay');
const partCount = document.getElementById('sh-part-count');
const partDirection = document.getElementById('sh-part-direction');
const partFill = document.getElementById('sh-part-fill');
let state = {
duration: 2, delay: 0, timing: 'ease-in',
count: '1', direction: 'normal', fill: 'forwards'
};
let watchdog = null;
const notes = {
duration: '<strong>Seven values, one property.</strong> <code>animation</code> is shorthand for <code>-name -duration -timing-function -delay -iteration-count -direction -fill-mode</code>. Duration is how long one cycle takes; delay is a pause before it starts.',
timing: '<strong>animation-timing-function</strong> shapes the pace within each cycle — <code>ease-in</code> starts slow, <code>ease-out</code> ends slow, <code>linear</code> is constant speed throughout.',
repeat: '<strong>iteration-count</strong> sets how many cycles run (or <code>infinite</code>). <strong>direction</strong> decides whether repeats reset to the start each time (<code>normal</code>) or reverse every other cycle (<code>alternate</code>) — only visible with count > 1.',
fill: '<strong>fill-mode</strong> controls the state outside the animation\'s active time: <code>forwards</code> keeps the end state, <code>backwards</code> applies the first keyframe during the delay, <code>both</code> does both, <code>none</code> reverts to the box\'s own CSS the instant it\'s not actively running.'
};
function highlightParts(tab) {
allParts.forEach(p => p.classList.remove('sh-part-on'));
parts[tab].forEach(p => p.classList.add('sh-part-on'));
}
function render() {
partDuration.textContent = state.duration + 's';
partTiming.textContent = state.timing;
partDelay.textContent = state.delay + 's';
partCount.textContent = state.count;
partDirection.textContent = state.direction;
partFill.textContent = state.fill;
durationReadout.textContent = state.duration + 's';
delayReadout.textContent = state.delay + 's';
}
function play() {
if (watchdog) { clearTimeout(watchdog); watchdog = null; }
box.classList.remove('sh-playing');
box.style.opacity = state.fill === 'backwards' || state.fill === 'both' ? '' : '0';
box.style.animationDuration = state.duration + 's';
box.style.animationDelay = state.delay + 's';
box.style.animationTimingFunction = state.timing;
box.style.animationIterationCount = state.count;
box.style.animationDirection = state.direction;
box.style.animationFillMode = state.fill;
void box.offsetWidth;
box.classList.add('sh-playing');
statusText.textContent = state.delay > 0 ? 'delay…' : 'running…';
const delayMs = state.delay * 1000;
const runMs = state.count === 'infinite' ? Infinity : state.duration * 1000 * Number(state.count);
if (delayMs > 0) {
watchdog = setTimeout(() => { statusText.textContent = 'running…'; }, delayMs);
}
if (runMs !== Infinity) {
setTimeout(() => { statusText.textContent = 'finished (' + state.fill + ')'; }, delayMs + runMs);
} else {
setTimeout(() => { statusText.textContent = 'looping…'; }, delayMs + 50);
}
}
typeButtons.forEach(btn => {
btn.addEventListener('click', () => {
const tab = btn.getAttribute('data-tab');
typeButtons.forEach(b => b.classList.toggle('sh-type-on', b === btn));
Object.entries(groups).forEach(([key, g]) => g.classList.toggle('sh-group-on', key === tab));
highlightParts(tab);
note.innerHTML = notes[tab];
});
});
replayBtn.addEventListener('click', play);
durationRange.addEventListener('input', () => { state.duration = Number(durationRange.value); render(); });
delayRange.addEventListener('input', () => { state.delay = Number(delayRange.value); render(); });
timingSeg.forEach(btn => btn.addEventListener('click', () => {
state.timing = btn.getAttribute('data-timing');
timingSeg.forEach(b => b.classList.toggle('sh-seg-on', b === btn));
render();
}));
countSeg.forEach(btn => btn.addEventListener('click', () => {
state.count = btn.getAttribute('data-count');
countSeg.forEach(b => b.classList.toggle('sh-seg-on', b === btn));
render();
}));
directionSeg.forEach(btn => btn.addEventListener('click', () => {
state.direction = btn.getAttribute('data-direction');
directionSeg.forEach(b => b.classList.toggle('sh-seg-on', b === btn));
render();
}));
fillSeg.forEach(btn => btn.addEventListener('click', () => {
state.fill = btn.getAttribute('data-fill');
fillSeg.forEach(b => b.classList.toggle('sh-seg-on', b === btn));
render();
}));
render();
highlightParts('duration');
setTimeout(play, 400);
})();
</script>
</body>
</html>🔹 Example: Slide In from Left
<body>
<div class="box">Sliding In!</div>
</body><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Slide In Animation</title>
<style>
.infographic-slidein {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
box-sizing: border-box;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--paper);
color: var(--ink);
border-radius: 14px;
overflow: hidden;
}
.infographic-slidein[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
}
.si-inner { padding: 0.5rem; }
.si-layout { display: flex; flex-direction: column; gap: 1.2rem; }
.si-typebar { display: flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; align-self: flex-start; }
.si-type-btn { border: none; background: var(--paper); color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.75rem; font-weight: 700; padding: 0.4rem 0.9rem; cursor: pointer; }
.si-type-btn + .si-type-btn { border-left: 1px solid var(--line); }
.si-type-btn.si-type-on { background: var(--accent); color: #fff; }
.si-code-wrap { background: #0b0b0b; border: 1px solid #2a2a2a; border-radius: 10px; overflow: hidden; }
.si-filetabs { display: flex; background: #161616; border-bottom: 1px solid #2a2a2a; }
.si-filetab { border: none; background: transparent; color: #fff; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.78rem; font-weight: 700; padding: 0.6rem 1rem; border-bottom: 2px solid #5b8def; }
.si-code { padding: 1rem 1.8rem 1rem 0.9rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.76rem; line-height: 1.85; overflow-x: auto; }
.si-line { display: block; white-space: pre; color: #e5e5e5; padding: 0.05rem 0.4rem; border-radius: 5px; }
.si-line.si-hl { background: #1c2333; box-shadow: inset 3px 0 0 #5b8def; color: #ffffff; }
.si-sel { color: #f472b6; font-weight: 700; }
.si-prop { color: #c586c0; }
.si-val { color: #ce9178; }
.si-cmt { color: #6a737d; }
.si-at { color: #60a5fa; font-weight: 700; }
.si-browser { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--paper); }
.si-browser-bar { background: var(--paper-alt); padding: 0.5rem 0.7rem; display: flex; align-items: center; gap: 0.5rem; border-bottom: 1px solid var(--line); }
.si-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--line); }
.si-tab { background: var(--paper); border: 1px solid var(--line); border-bottom: none; border-radius: 6px 6px 0 0; font-size: 0.72rem; color: var(--ink-soft); padding: 0.25rem 0.6rem; }
.si-viewport {
margin: 1.4rem 1rem 0.9rem;
height: 100px;
overflow: hidden;
position: relative;
border: 1px dashed var(--line);
border-radius: 8px;
background:
repeating-linear-gradient(45deg, var(--paper-alt) 0 8px, var(--paper) 8px 16px);
}
.si-viewport-label {
position: absolute;
top: 4px; left: 8px;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 0.62rem;
color: var(--ink-soft);
opacity: 0.7;
z-index: 2;
}
.si-box {
width: 100%; height: 100px;
background-color: #27ae60;
color: white;
display: flex; align-items: center; justify-content: center;
font-family: sans-serif; font-size: 1rem;
border-radius: 10px;
box-sizing: border-box;
transform: translateX(-100%);
}
.si-box.si-playing { animation-name: si-slideIn; }
@keyframes si-slideIn {
from { transform: translateX(-100%); }
to { transform: translateX(0); }
}
.si-timeline { margin: 0 1rem 1rem; position: relative; height: 4px; background: var(--line); border-radius: 2px; }
.si-timeline-fill { position: absolute; left: 0; top: 0; height: 100%; background: var(--accent); border-radius: 2px; width: 0%; }
.si-timeline-marks { display: flex; justify-content: space-between; margin: 0 1rem 0.6rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.65rem; color: var(--ink-soft); }
.si-controls { background: var(--paper-alt); padding: 0.8rem 0.9rem 1rem; border-top: 1px solid var(--line); }
.si-control-group { display: none; flex-direction: column; gap: 0.7rem; }
.si-control-group.si-group-on { display: flex; }
.si-control-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.si-control-label { font-size: 0.72rem; color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; min-width: 70px; }
.si-range { flex: 1; min-width: 100px; accent-color: var(--accent); cursor: pointer; }
.si-readout { font-size: 0.78rem; color: var(--ink); font-weight: 700; font-family: "SFMono-Regular", Consolas, monospace; min-width: 55px; text-align: right; }
.si-replay-btn { border: 1px solid var(--accent); background: var(--accent); color: #fff; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.75rem; font-weight: 700; padding: 0.45rem 1rem; border-radius: 999px; cursor: pointer; }
.si-segmented { display: flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; flex-wrap: wrap; }
.si-seg-btn { border: none; background: var(--paper); color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.68rem; font-weight: 700; padding: 0.32rem 0.6rem; cursor: pointer; white-space: nowrap; }
.si-seg-btn + .si-seg-btn { border-left: 1px solid var(--line); }
.si-seg-btn.si-seg-on { background: var(--accent); color: #fff; }
.si-note { font-size: 0.82rem; color: var(--ink-soft); padding: 0 0.2rem; }
.si-note strong { color: var(--ink); }
.si-note code { font-family: "SFMono-Regular", Consolas, monospace; background: var(--paper-alt); padding: 0.05rem 0.3rem; border-radius: 4px; }
</style>
</head>
<body>
<div class="infographic-slidein" id="si-root">
<div class="si-inner">
<div class="si-layout">
<div class="si-typebar" id="si-typebar">
<button class="si-type-btn si-type-on" data-tab="replay">Replay</button>
<button class="si-type-btn" data-tab="scrub">Scrub keyframes</button>
</div>
<div class="si-code-wrap">
<div class="si-filetabs">
<button class="si-filetab">styles.css</button>
</div>
<div class="si-code">
<span class="si-line"><span class="si-at">@keyframes</span> <span class="si-val">slideIn</span> {</span>
<span class="si-line" id="si-line-from"> <span class="si-val">from</span> { <span class="si-prop">transform</span>: <span class="si-val">translateX(-100%)</span>; }</span>
<span class="si-line" id="si-line-to"> <span class="si-val">to</span> { <span class="si-prop">transform</span>: <span class="si-val">translateX(0)</span>; }</span>
<span class="si-line">}</span>
<span class="si-line"></span>
<span class="si-line"><span class="si-sel">.box</span> {</span>
<span class="si-line"> <span class="si-prop">width</span>: <span class="si-val">250px</span>;</span>
<span class="si-line"> <span class="si-prop">height</span>: <span class="si-val">100px</span>;</span>
<span class="si-line"> <span class="si-prop">background-color</span>: <span class="si-val">#27ae60</span>;</span>
<span class="si-line"></span>
<span class="si-line" id="si-line-anim"> <span class="si-prop">animation</span>: <span class="si-val">slideIn</span> <span class="si-val" id="si-val-duration">1s</span> <span class="si-val" id="si-val-timing">ease-out</span> <span class="si-val">forwards</span>;</span>
<span class="si-line">}</span>
</div>
</div>
<div class="si-browser">
<div class="si-browser-bar">
<span class="si-dot"></span><span class="si-dot"></span><span class="si-dot"></span>
<div class="si-tab">🌐 Slide In</div>
</div>
<div class="si-viewport">
<span class="si-viewport-label">card edge — overflow: hidden</span>
<div class="si-box" id="si-box">Sliding In!</div>
</div>
<div class="si-timeline"><div class="si-timeline-fill" id="si-timeline-fill"></div></div>
<div class="si-timeline-marks"><span>-100%</span><span>-50%</span><span>0</span></div>
<div class="si-controls">
<div class="si-control-group si-group-on" id="si-group-replay">
<div class="si-control-row">
<button class="si-replay-btn" id="si-replay">▶ Replay</button>
<span class="si-control-label" style="min-width:auto;">duration</span>
<input type="range" class="si-range" id="si-duration" min="0.3" max="3" step="0.1" value="1">
<span class="si-readout" id="si-duration-readout">1s</span>
</div>
<div class="si-control-row">
<span class="si-control-label">timing</span>
<div class="si-segmented" id="si-timing-seg">
<button class="si-seg-btn" data-timing="ease-in">ease-in</button>
<button class="si-seg-btn si-seg-on" data-timing="ease-out">ease-out</button>
<button class="si-seg-btn" data-timing="linear">linear</button>
<button class="si-seg-btn" data-timing="cubic-bezier(.34,1.56,.64,1)">bouncy</button>
</div>
</div>
</div>
<div class="si-control-group" id="si-group-scrub">
<div class="si-control-row">
<span class="si-control-label">progress</span>
<input type="range" class="si-range" id="si-scrub" min="0" max="100" step="1" value="0">
<span class="si-readout" id="si-scrub-readout">-100%</span>
</div>
</div>
</div>
</div>
<div class="si-note" id="si-note">
<strong>translateX(-100%)</strong> is relative to the box's <em>own</em> width, not the viewport — a 250px box moves exactly 250px left, placing it just past its own left edge. Pairing that with <code>overflow: hidden</code> on a wrapper (the dashed card here) is what actually hides it off-screen before the animation runs.
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-slidein')
|| document.querySelector('.infographic-slidein');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
})();
(function () {
const typeButtons = document.querySelectorAll('.si-type-btn');
const groups = {
replay: document.getElementById('si-group-replay'),
scrub: document.getElementById('si-group-scrub')
};
const box = document.getElementById('si-box');
const note = document.getElementById('si-note');
const timelineFill = document.getElementById('si-timeline-fill');
const replayBtn = document.getElementById('si-replay');
const durationRange = document.getElementById('si-duration');
const durationReadout = document.getElementById('si-duration-readout');
const valDuration = document.getElementById('si-val-duration');
const valTiming = document.getElementById('si-val-timing');
const timingSeg = document.querySelectorAll('#si-timing-seg .si-seg-btn');
const lineAnim = document.getElementById('si-line-anim');
const lineFrom = document.getElementById('si-line-from');
const lineTo = document.getElementById('si-line-to');
const scrubRange = document.getElementById('si-scrub');
const scrubReadout = document.getElementById('si-scrub-readout');
let duration = 1;
let timing = 'ease-out';
let animTimer = null;
let animStart = null;
function switchTab(tab) {
typeButtons.forEach(b => b.classList.toggle('si-type-on', b.getAttribute('data-tab') === tab));
Object.entries(groups).forEach(([key, g]) => g.classList.toggle('si-group-on', key === tab));
if (tab === 'scrub') {
box.classList.remove('si-playing');
if (animTimer) { clearInterval(animTimer); animTimer = null; }
applyScrub(Number(scrubRange.value));
note.innerHTML = '<strong>Drag the progress slider</strong> to move the box between <code>-100%</code> and <code>0</code> by hand — the same interpolation the browser runs automatically once you hit Replay.';
} else {
resetToStart();
note.innerHTML = '<strong>translateX(-100%)</strong> is relative to the box\'s <em>own</em> width, not the viewport — a 250px box moves exactly 250px left. Pairing that with <code>overflow: hidden</code> on a wrapper is what hides it off-screen before the animation runs.';
}
}
function applyScrub(pct) {
const t = pct / 100;
const x = -100 + 100 * t;
box.style.transform = `translateX(${x}%)`;
timelineFill.style.width = pct + '%';
scrubReadout.textContent = x.toFixed(0) + '%';
lineFrom.classList.toggle('si-hl', pct < 50);
lineTo.classList.toggle('si-hl', pct >= 50);
}
function resetToStart() {
box.classList.remove('si-playing');
box.style.transform = '';
timelineFill.style.width = '0%';
lineFrom.classList.remove('si-hl');
lineTo.classList.remove('si-hl');
}
function playReplay() {
if (animTimer) { clearInterval(animTimer); animTimer = null; }
box.classList.remove('si-playing');
box.style.transform = '';
box.style.animationDuration = duration + 's';
box.style.animationTimingFunction = timing;
void box.offsetWidth;
box.classList.add('si-playing');
timelineFill.style.width = '0%';
animStart = performance.now();
animTimer = setInterval(() => {
const elapsed = (performance.now() - animStart) / 1000;
const pct = Math.min(100, (elapsed / duration) * 100);
timelineFill.style.width = pct + '%';
lineFrom.classList.toggle('si-hl', pct < 50);
lineTo.classList.toggle('si-hl', pct >= 50);
if (pct >= 100) { clearInterval(animTimer); animTimer = null; }
}, 30);
}
function render() {
valDuration.textContent = duration + 's';
valTiming.textContent = timing;
durationReadout.textContent = duration + 's';
}
typeButtons.forEach(btn => btn.addEventListener('click', () => switchTab(btn.getAttribute('data-tab'))));
replayBtn.addEventListener('click', () => {
lineAnim.classList.remove('si-hl');
playReplay();
});
durationRange.addEventListener('input', () => {
duration = Number(durationRange.value);
render();
lineAnim.classList.add('si-hl');
});
timingSeg.forEach(btn => {
btn.addEventListener('click', () => {
timing = btn.getAttribute('data-timing');
timingSeg.forEach(b => b.classList.toggle('si-seg-on', b === btn));
render();
lineAnim.classList.add('si-hl');
});
});
scrubRange.addEventListener('input', () => applyScrub(Number(scrubRange.value)));
render();
resetToStart();
setTimeout(playReplay, 400);
})();
</script>
</body>
</html>🔹Looping Animation (e.g., Pulse)
<body>
<div class="pulse-box">Pulse</div>
</body><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Pulse Animation</title>
<style>
.infographic-pulse {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
box-sizing: border-box;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--paper);
color: var(--ink);
border-radius: 14px;
overflow: hidden;
}
.infographic-pulse[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
}
.pu-inner { padding: 0.5rem; }
.pu-layout { display: flex; flex-direction: column; gap: 1.2rem; }
.pu-typebar { display: flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; align-self: flex-start; }
.pu-type-btn { border: none; background: var(--paper); color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.75rem; font-weight: 700; padding: 0.4rem 0.9rem; cursor: pointer; }
.pu-type-btn + .pu-type-btn { border-left: 1px solid var(--line); }
.pu-type-btn.pu-type-on { background: var(--accent); color: #fff; }
.pu-code-wrap { background: #0b0b0b; border: 1px solid #2a2a2a; border-radius: 10px; overflow: hidden; }
.pu-filetabs { display: flex; background: #161616; border-bottom: 1px solid #2a2a2a; }
.pu-filetab { border: none; background: transparent; color: #fff; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.78rem; font-weight: 700; padding: 0.6rem 1rem; border-bottom: 2px solid #5b8def; }
.pu-code { padding: 1rem 1.8rem 1rem 0.9rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.76rem; line-height: 1.85; overflow-x: auto; }
.pu-line { display: block; white-space: pre; color: #e5e5e5; padding: 0.05rem 0.4rem; border-radius: 5px; }
.pu-line.pu-hl { background: #1c2333; box-shadow: inset 3px 0 0 #5b8def; color: #ffffff; }
.pu-sel { color: #f472b6; font-weight: 700; }
.pu-prop { color: #c586c0; }
.pu-val { color: #ce9178; }
.pu-cmt { color: #6a737d; }
.pu-at { color: #60a5fa; font-weight: 700; }
.pu-browser { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--paper); }
.pu-browser-bar { background: var(--paper-alt); padding: 0.5rem 0.7rem; display: flex; align-items: center; gap: 0.5rem; border-bottom: 1px solid var(--line); }
.pu-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--line); }
.pu-tab { background: var(--paper); border: 1px solid var(--line); border-bottom: none; border-radius: 6px 6px 0 0; font-size: 0.72rem; color: var(--ink-soft); padding: 0.25rem 0.6rem; }
.pu-screen {
padding: 2rem 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
background: var(--paper-alt);
min-height: 190px;
justify-content: center;
}
.pu-stage { position: relative; width: 180px; height: 180px; display: flex; align-items: center; justify-content: center; }
.pu-box-outline {
position: absolute;
width: 150px; height: 150px;
border: 1.5px dashed var(--ink-soft);
border-radius: 50%;
opacity: 0.35;
}
.pu-box {
width: 150px; height: 150px;
background-color: #e91e63;
color: white;
display: flex; align-items: center; justify-content: center;
font-family: sans-serif; font-size: 1rem;
border-radius: 50%;
box-sizing: border-box;
transform: scale(1);
}
.pu-box.pu-playing { animation-name: pu-pulse; animation-iteration-count: infinite; }
@keyframes pu-pulse {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.1); }
}
.pu-timeline { width: 200px; position: relative; height: 4px; background: var(--line); border-radius: 2px; }
.pu-timeline-fill { position: absolute; left: 0; top: 0; height: 100%; background: var(--accent); border-radius: 2px; width: 0%; }
.pu-timeline-marks { display: flex; justify-content: space-between; width: 200px; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.65rem; color: var(--ink-soft); }
.pu-controls { background: var(--paper-alt); padding: 0.8rem 0.9rem 1rem; border-top: 1px solid var(--line); }
.pu-control-group { display: none; flex-direction: column; gap: 0.7rem; }
.pu-control-group.pu-group-on { display: flex; }
.pu-control-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.pu-control-label { font-size: 0.72rem; color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; min-width: 70px; }
.pu-range { flex: 1; min-width: 100px; accent-color: var(--accent); cursor: pointer; }
.pu-readout { font-size: 0.78rem; color: var(--ink); font-weight: 700; font-family: "SFMono-Regular", Consolas, monospace; min-width: 45px; text-align: right; }
.pu-play-btn { border: 1px solid var(--accent); background: var(--accent); color: #fff; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.75rem; font-weight: 700; padding: 0.45rem 1rem; border-radius: 999px; cursor: pointer; }
.pu-play-btn.pu-paused { background: var(--paper); color: var(--accent); }
.pu-segmented { display: flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; flex-wrap: wrap; }
.pu-seg-btn { border: none; background: var(--paper); color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.68rem; font-weight: 700; padding: 0.32rem 0.6rem; cursor: pointer; white-space: nowrap; }
.pu-seg-btn + .pu-seg-btn { border-left: 1px solid var(--line); }
.pu-seg-btn.pu-seg-on { background: var(--accent); color: #fff; }
.pu-note { font-size: 0.82rem; color: var(--ink-soft); padding: 0 0.2rem; }
.pu-note strong { color: var(--ink); }
.pu-note code { font-family: "SFMono-Regular", Consolas, monospace; background: var(--paper-alt); padding: 0.05rem 0.3rem; border-radius: 4px; }
</style>
</head>
<body>
<div class="infographic-pulse" id="pu-root">
<div class="pu-inner">
<div class="pu-layout">
<div class="pu-typebar" id="pu-typebar">
<button class="pu-type-btn pu-type-on" data-tab="live">Live</button>
<button class="pu-type-btn" data-tab="scrub">Scrub one cycle</button>
</div>
<div class="pu-code-wrap">
<div class="pu-filetabs">
<button class="pu-filetab">styles.css</button>
</div>
<div class="pu-code">
<span class="pu-line"><span class="pu-at">@keyframes</span> <span class="pu-val">pulse</span> {</span>
<span class="pu-line" id="pu-line-ends"> <span class="pu-val">0%, 100%</span> { <span class="pu-prop">transform</span>: <span class="pu-val">scale(1)</span>; } <span class="pu-cmt">/* normal */</span></span>
<span class="pu-line" id="pu-line-mid"> <span class="pu-val">50%</span> { <span class="pu-prop">transform</span>: <span class="pu-val">scale(1.1)</span>; } <span class="pu-cmt">/* grows */</span></span>
<span class="pu-line">}</span>
<span class="pu-line"></span>
<span class="pu-line"><span class="pu-sel">.pulse-box</span> {</span>
<span class="pu-line"> <span class="pu-prop">width</span>: <span class="pu-val">150px</span>;</span>
<span class="pu-line"> <span class="pu-prop">height</span>: <span class="pu-val">150px</span>;</span>
<span class="pu-line"> <span class="pu-prop">background-color</span>: <span class="pu-val">#e91e63</span>;</span>
<span class="pu-line"> <span class="pu-prop">border-radius</span>: <span class="pu-val">50%</span>;</span>
<span class="pu-line" id="pu-line-anim"> <span class="pu-prop">animation</span>: <span class="pu-val">pulse</span> <span class="pu-val" id="pu-val-duration">1.5s</span> <span class="pu-val">infinite</span>;</span>
<span class="pu-line">}</span>
</div>
</div>
<div class="pu-browser">
<div class="pu-browser-bar">
<span class="pu-dot"></span><span class="pu-dot"></span><span class="pu-dot"></span>
<div class="pu-tab">🌐 Pulse</div>
</div>
<div class="pu-screen">
<div class="pu-stage">
<div class="pu-box-outline"></div>
<div class="pu-box pu-playing" id="pu-box">Pulse</div>
</div>
<div>
<div class="pu-timeline"><div class="pu-timeline-fill" id="pu-timeline-fill"></div></div>
<div class="pu-timeline-marks"><span>0%</span><span>50%</span><span>100%</span></div>
</div>
</div>
<div class="pu-controls">
<div class="pu-control-group pu-group-on" id="pu-group-live">
<div class="pu-control-row">
<button class="pu-play-btn" id="pu-toggle">⏸ Pause</button>
<span class="pu-control-label" style="min-width:auto;">speed</span>
<input type="range" class="pu-range" id="pu-duration" min="0.4" max="4" step="0.1" value="1.5">
<span class="pu-readout" id="pu-duration-readout">1.5s</span>
</div>
</div>
<div class="pu-control-group" id="pu-group-scrub">
<div class="pu-control-row">
<span class="pu-control-label">progress</span>
<input type="range" class="pu-range" id="pu-scrub" min="0" max="100" step="1" value="0">
<span class="pu-readout" id="pu-scrub-readout">0%</span>
</div>
</div>
</div>
</div>
<div class="pu-note" id="pu-note">
<strong>0% and 100% share one keyframe block</strong> — a comma-separated selector, just like in CSS. Because both ends match the resting <code>scale(1)</code>, the loop has nowhere jarring to reset: it always grows out to 50% and eases back to the same size it started at.
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-pulse')
|| document.querySelector('.infographic-pulse');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
})();
(function () {
const typeButtons = document.querySelectorAll('.pu-type-btn');
const groups = {
live: document.getElementById('pu-group-live'),
scrub: document.getElementById('pu-group-scrub')
};
const box = document.getElementById('pu-box');
const note = document.getElementById('pu-note');
const timelineFill = document.getElementById('pu-timeline-fill');
const toggleBtn = document.getElementById('pu-toggle');
const durationRange = document.getElementById('pu-duration');
const durationReadout = document.getElementById('pu-duration-readout');
const valDuration = document.getElementById('pu-val-duration');
const lineAnim = document.getElementById('pu-line-anim');
const lineEnds = document.getElementById('pu-line-ends');
const lineMid = document.getElementById('pu-line-mid');
const scrubRange = document.getElementById('pu-scrub');
const scrubReadout = document.getElementById('pu-scrub-readout');
let duration = 1.5;
let playing = true;
let mode = 'live';
let rafId = null;
let cycleStart = performance.now();
function tick(now) {
if (mode === 'live' && playing) {
const elapsed = ((now - cycleStart) / 1000) % duration;
const pct = (elapsed / duration) * 100;
timelineFill.style.width = pct + '%';
lineEnds.classList.toggle('pu-hl', pct < 25 || pct > 75);
lineMid.classList.toggle('pu-hl', pct >= 25 && pct <= 75);
}
rafId = requestAnimationFrame(tick);
}
function switchTab(tab) {
mode = tab;
typeButtons.forEach(b => b.classList.toggle('pu-type-on', b.getAttribute('data-tab') === tab));
Object.entries(groups).forEach(([key, g]) => g.classList.toggle('pu-group-on', key === tab));
if (tab === 'scrub') {
box.classList.remove('pu-playing');
applyScrub(Number(scrubRange.value));
note.innerHTML = '<strong>Drag through one cycle</strong> to see the scale value at any point — 0% and 100% land on the same <code>scale(1)</code>, so a fresh loop is invisible to the eye.';
} else {
box.style.transform = '';
box.classList.add('pu-playing');
box.style.animationPlayState = playing ? 'running' : 'paused';
cycleStart = performance.now();
note.innerHTML = '<strong>0% and 100% share one keyframe block</strong> — a comma-separated selector, just like in CSS. Because both ends match the resting <code>scale(1)</code>, the loop has nowhere jarring to reset.';
}
}
function applyScrub(pct) {
const s = pct <= 50
? 1 + (0.1 * (pct / 50))
: 1.1 - (0.1 * ((pct - 50) / 50));
box.style.transform = `scale(${s.toFixed(3)})`;
timelineFill.style.width = pct + '%';
scrubReadout.textContent = pct + '%';
lineEnds.classList.toggle('pu-hl', pct < 25 || pct > 75);
lineMid.classList.toggle('pu-hl', pct >= 25 && pct <= 75);
}
toggleBtn.addEventListener('click', () => {
playing = !playing;
box.style.animationPlayState = playing ? 'running' : 'paused';
toggleBtn.textContent = playing ? '⏸ Pause' : '▶ Play';
toggleBtn.classList.toggle('pu-paused', !playing);
if (playing) cycleStart = performance.now();
});
durationRange.addEventListener('input', () => {
duration = Number(durationRange.value);
box.style.animationDuration = duration + 's';
valDuration.textContent = duration + 's';
durationReadout.textContent = duration + 's';
lineAnim.classList.add('pu-hl');
cycleStart = performance.now();
});
typeButtons.forEach(btn => btn.addEventListener('click', () => switchTab(btn.getAttribute('data-tab'))));
scrubRange.addEventListener('input', () => applyScrub(Number(scrubRange.value)));
box.style.animationDuration = duration + 's';
requestAnimationFrame(tick);
})();
</script>
</body>
</html>🔹 Combining Multiple Properties
<body>
<div class="bounce-box">Bounce!</div>
</body><!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Bounce Animation</title>
<style>
.infographic-bounce {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
box-sizing: border-box;
margin: 0 auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--paper);
color: var(--ink);
border-radius: 14px;
overflow: hidden;
}
.infographic-bounce[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
}
.bo-inner { padding: 0.5rem; }
.bo-layout { display: flex; flex-direction: column; gap: 1.2rem; }
.bo-code-wrap { background: #0b0b0b; border: 1px solid #2a2a2a; border-radius: 10px; overflow: hidden; }
.bo-filetabs { display: flex; background: #161616; border-bottom: 1px solid #2a2a2a; }
.bo-filetab { border: none; background: transparent; color: #8a8a8a; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.78rem; font-weight: 700; padding: 0.6rem 1rem; cursor: pointer; border-bottom: 2px solid transparent; }
.bo-filetab:hover { color: #e5e5e5; }
.bo-filetab.bo-tab-on { color: #fff; border-bottom-color: #5b8def; background: #0b0b0b; }
.bo-code { padding: 1rem 1.8rem 1rem 0.9rem; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.76rem; line-height: 1.85; overflow-x: auto; }
.bo-codepane { display: none; }
.bo-codepane.bo-pane-on { display: block; }
.bo-line { display: block; white-space: pre; color: #e5e5e5; padding: 0.05rem 0.4rem; border-radius: 5px; }
.bo-line.bo-hl { background: #1c2333; box-shadow: inset 3px 0 0 #5b8def; color: #ffffff; }
.bo-sel { color: #f472b6; font-weight: 700; }
.bo-prop { color: #c586c0; }
.bo-val { color: #ce9178; }
.bo-cmt { color: #6a737d; }
.bo-at { color: #60a5fa; font-weight: 700; }
.bo-browser { border: 1px solid var(--line); border-radius: 10px; overflow: hidden; background: var(--paper); }
.bo-browser-bar { background: var(--paper-alt); padding: 0.5rem 0.7rem; display: flex; align-items: center; gap: 0.5rem; border-bottom: 1px solid var(--line); }
.bo-dot { width: 9px; height: 9px; border-radius: 50%; background: var(--line); }
.bo-tab { background: var(--paper); border: 1px solid var(--line); border-bottom: none; border-radius: 6px 6px 0 0; font-size: 0.72rem; color: var(--ink-soft); padding: 0.25rem 0.6rem; }
.bo-screen {
padding: 1.6rem 1rem 2.2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
background: var(--paper-alt);
min-height: 190px;
justify-content: flex-end;
}
.bo-stage { position: relative; width: 180px; height: 150px; display: flex; align-items: flex-end; justify-content: center; }
.bo-floor { position: absolute; bottom: 0; left: 10px; right: 10px; height: 1.5px; background: var(--ink-soft); opacity: 0.4; }
.bo-box {
width: 150px; height: 100px;
background-color: #f39c12;
color: white;
display: flex; align-items: center; justify-content: center;
font-family: sans-serif; font-size: 1rem;
border-radius: 10px;
box-sizing: border-box;
transform: translateY(0);
}
.bo-box.bo-playing { animation-name: bo-bounce; animation-iteration-count: infinite; }
@keyframes bo-bounce {
0% { transform: translateY(0); }
100% { transform: translateY(-30px); }
}
.bo-controls { background: var(--paper-alt); padding: 0.8rem 0.9rem 1rem; display: flex; flex-direction: column; gap: 0.7rem; border-top: 1px solid var(--line); }
.bo-control-row { display: flex; align-items: center; gap: 0.6rem; flex-wrap: wrap; }
.bo-control-label { font-size: 0.72rem; color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; min-width: 70px; }
.bo-range { flex: 1; min-width: 100px; accent-color: var(--accent); cursor: pointer; }
.bo-readout { font-size: 0.78rem; color: var(--ink); font-weight: 700; font-family: "SFMono-Regular", Consolas, monospace; min-width: 45px; text-align: right; }
.bo-play-btn { border: 1px solid var(--accent); background: var(--accent); color: #fff; font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.75rem; font-weight: 700; padding: 0.45rem 1rem; border-radius: 999px; cursor: pointer; }
.bo-play-btn.bo-paused { background: var(--paper); color: var(--accent); }
.bo-segmented { display: flex; border: 1px solid var(--line); border-radius: 999px; overflow: hidden; flex-wrap: wrap; }
.bo-seg-btn { border: none; background: var(--paper); color: var(--ink-soft); font-family: "SFMono-Regular", Consolas, monospace; font-size: 0.68rem; font-weight: 700; padding: 0.32rem 0.6rem; cursor: pointer; white-space: nowrap; }
.bo-seg-btn + .bo-seg-btn { border-left: 1px solid var(--line); }
.bo-seg-btn.bo-seg-on { background: var(--accent); color: #fff; }
.bo-note { font-size: 0.82rem; color: var(--ink-soft); padding: 0 0.2rem; }
.bo-note strong { color: var(--ink); }
.bo-note code { font-family: "SFMono-Regular", Consolas, monospace; background: var(--paper-alt); padding: 0.05rem 0.3rem; border-radius: 4px; }
</style>
</head>
<body>
<div class="infographic-bounce" id="bo-root">
<div class="bo-inner">
<div class="bo-layout">
<div class="bo-code-wrap">
<div class="bo-filetabs">
<button class="bo-filetab bo-tab-on" id="bo-tab-long">longhand</button>
<button class="bo-filetab" id="bo-tab-short">shorthand</button>
</div>
<div class="bo-code">
<div class="bo-codepane bo-pane-on" id="bo-pane-long">
<span class="bo-line"><span class="bo-at">@keyframes</span> <span class="bo-val">bounce</span> {</span>
<span class="bo-line"> <span class="bo-val">0%</span> { <span class="bo-prop">transform</span>: <span class="bo-val">translateY(0)</span>; }</span>
<span class="bo-line"> <span class="bo-val">100%</span> { <span class="bo-prop">transform</span>: <span class="bo-val">translateY(-30px)</span>; }</span>
<span class="bo-line">}</span>
<span class="bo-line"></span>
<span class="bo-line"><span class="bo-sel">.bounce-box</span> {</span>
<span class="bo-line"> <span class="bo-prop">width</span>: <span class="bo-val">150px</span>;</span>
<span class="bo-line"> <span class="bo-prop">height</span>: <span class="bo-val">100px</span>;</span>
<span class="bo-line"> <span class="bo-prop">background-color</span>: <span class="bo-val">#f39c12</span>;</span>
<span class="bo-line"></span>
<span class="bo-line"> <span class="bo-prop">animation-name</span>: <span class="bo-val">bounce</span>;</span>
<span class="bo-line" id="bo-long-duration"> <span class="bo-prop">animation-duration</span>: <span class="bo-val" id="bo-val-duration-l">1s</span>;</span>
<span class="bo-line" id="bo-long-timing"> <span class="bo-prop">animation-timing-function</span>: <span class="bo-val" id="bo-val-timing-l">ease-in-out</span>;</span>
<span class="bo-line"> <span class="bo-prop">animation-iteration-count</span>: <span class="bo-val">infinite</span>;</span>
<span class="bo-line" id="bo-long-direction"> <span class="bo-prop">animation-direction</span>: <span class="bo-val" id="bo-val-direction-l">alternate</span>;</span>
<span class="bo-line">}</span>
</div>
<div class="bo-codepane" id="bo-pane-short">
<span class="bo-line"><span class="bo-at">@keyframes</span> <span class="bo-val">bounce</span> { <span class="bo-cmt">/* same as above */</span> }</span>
<span class="bo-line"></span>
<span class="bo-line"><span class="bo-sel">.bounce-box</span> {</span>
<span class="bo-line"> <span class="bo-prop">width</span>: <span class="bo-val">150px</span>;</span>
<span class="bo-line"> <span class="bo-prop">height</span>: <span class="bo-val">100px</span>;</span>
<span class="bo-line"> <span class="bo-prop">background-color</span>: <span class="bo-val">#f39c12</span>;</span>
<span class="bo-line"></span>
<span class="bo-line" id="bo-short-line"> <span class="bo-prop">animation</span>: <span class="bo-val">bounce</span> <span class="bo-val" id="bo-val-duration-s">1s</span> <span class="bo-val" id="bo-val-timing-s">ease-in-out</span> <span class="bo-val">infinite</span> <span class="bo-val" id="bo-val-direction-s">alternate</span>;</span>
<span class="bo-line">}</span>
</div>
</div>
</div>
<div class="bo-browser">
<div class="bo-browser-bar">
<span class="bo-dot"></span><span class="bo-dot"></span><span class="bo-dot"></span>
<div class="bo-tab">🌐 Bounce</div>
</div>
<div class="bo-screen">
<div class="bo-stage">
<div class="bo-box bo-playing" id="bo-box">Bounce!</div>
<div class="bo-floor"></div>
</div>
</div>
<div class="bo-controls">
<div class="bo-control-row">
<button class="bo-play-btn" id="bo-toggle">⏸ Pause</button>
<span class="bo-control-label" style="min-width:auto;">speed</span>
<input type="range" class="bo-range" id="bo-duration" min="0.3" max="2.5" step="0.1" value="1">
<span class="bo-readout" id="bo-duration-readout">1s</span>
</div>
<div class="bo-control-row">
<span class="bo-control-label">direction</span>
<div class="bo-segmented" id="bo-direction-seg">
<button class="bo-seg-btn" data-direction="normal">normal</button>
<button class="bo-seg-btn bo-seg-on" data-direction="alternate">alternate</button>
</div>
</div>
</div>
</div>
<div class="bo-note" id="bo-note">
<strong>Switch direction to "normal"</strong> and watch it break — the keyframes only go 0% → -30px, so without <code>alternate</code> reversing every other lap, the box snaps back down instantly at the end of each cycle instead of settling smoothly. <code>alternate</code> is what turns a one-way move into a back-and-forth bounce.
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-bounce')
|| document.querySelector('.infographic-bounce');
const bodyAttr = document.body.getAttribute('data-darkmode');
let dark;
if (bodyAttr === 'true') dark = true;
else if (bodyAttr === 'false') dark = false;
else {
const match = document.cookie.match(/(?:^|;\s*)darkMode=([^;]+)/);
dark = match ? match[1] === 'true' : false;
}
if (dark) wrapper.setAttribute('data-theme', 'dark');
})();
(function () {
const tabLong = document.getElementById('bo-tab-long');
const tabShort = document.getElementById('bo-tab-short');
const paneLong = document.getElementById('bo-pane-long');
const paneShort = document.getElementById('bo-pane-short');
tabLong.addEventListener('click', () => {
tabLong.classList.add('bo-tab-on'); tabShort.classList.remove('bo-tab-on');
paneLong.classList.add('bo-pane-on'); paneShort.classList.remove('bo-pane-on');
});
tabShort.addEventListener('click', () => {
tabShort.classList.add('bo-tab-on'); tabLong.classList.remove('bo-tab-on');
paneShort.classList.add('bo-pane-on'); paneLong.classList.remove('bo-pane-on');
});
const box = document.getElementById('bo-box');
const toggleBtn = document.getElementById('bo-toggle');
const durationRange = document.getElementById('bo-duration');
const durationReadout = document.getElementById('bo-duration-readout');
const directionSeg = document.querySelectorAll('#bo-direction-seg .bo-seg-btn');
const valDurationL = document.getElementById('bo-val-duration-l');
const valDurationS = document.getElementById('bo-val-duration-s');
const valDirectionL = document.getElementById('bo-val-direction-l');
const valDirectionS = document.getElementById('bo-val-direction-s');
let duration = 1;
let playing = true;
let direction = 'alternate';
function render() {
box.style.animationDuration = duration + 's';
box.style.animationDirection = direction;
box.style.animationPlayState = playing ? 'running' : 'paused';
valDurationL.textContent = duration + 's';
valDurationS.textContent = duration + 's';
valDirectionL.textContent = direction;
valDirectionS.textContent = direction;
durationReadout.textContent = duration + 's';
}
toggleBtn.addEventListener('click', () => {
playing = !playing;
toggleBtn.textContent = playing ? '⏸ Pause' : '▶ Play';
toggleBtn.classList.toggle('bo-paused', !playing);
render();
});
durationRange.addEventListener('input', () => {
duration = Number(durationRange.value);
render();
});
directionSeg.forEach(btn => {
btn.addEventListener('click', () => {
direction = btn.getAttribute('data-direction');
directionSeg.forEach(b => b.classList.toggle('bo-seg-on', b === btn));
render();
});
});
render();
})();
</script>
</body>
</html>