๐ฑ๏ธ Mouse Events
Mouse events detect how users interact using the mouse.
๐ Why Mouse Events Matter
React to user intention
Build interactive UI
Improve UX (hover, drag, menus)
๐งฉ Common Mouse Events
Event | Fires When |
|---|---|
| Mouse button clicked |
| Double click |
| Cursor enters element |
| Cursor leaves element |
| Cursor enters (no bubbling) |
| Cursor leaves (no bubbling) |
| Mouse moves |
<div class="infographic-mouseevents">
<style>
.infographic-mouseevents {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
--amber: #a4720a;
--amber-soft: #faf1de;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-mouseevents[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
--amber: #d9a94a;
--amber-soft: #2c2413;
}
.infographic-mouseevents * { box-sizing: border-box; }
.infographic-mouseevents h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-mouseevents p { margin: 0; }
.infographic-mouseevents button { font-family: inherit; }
.me-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.me-hero { text-align: center; margin-bottom: 1.2rem; }
.me-hero h1 { font-size: clamp(1.3rem, 4.5vw, 1.8rem); }
.me-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.84rem; }
.me-tabs { display: flex; flex-wrap: wrap; gap: 0.35rem; justify-content: center; margin-bottom: 1.2rem; }
.me-tab { border: 1px solid var(--line); background: var(--paper-alt); color: var(--ink-soft); padding: 0.34rem 0.6rem; font-size: 0.64rem; font-weight: 700; cursor: pointer; font-family: 'IBM Plex Mono', monospace; }
.me-tab.is-active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.me-tab:hover:not(.is-active) { border-color: var(--ink-soft); color: var(--ink); }
.me-panel { display: none; }
.me-panel.is-active { display: block; }
.me-code { background: #1e1e1e; color: #d4d4d4; border-radius: 5px; padding: 0.6rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; line-height: 1.6; margin-bottom: 1rem; white-space: pre; overflow-x: auto; }
.tok-kw { color: #569cd6; padding: 0; margin: 0; }
.tok-fn { color: #dcdcaa; padding: 0; margin: 0; }
.tok-str { color: #ce9178; padding: 0; margin: 0; }
.tok-var { color: #9cdcfe; padding: 0; margin: 0; }
.tok-punc { color: #d4d4d4; padding: 0; margin: 0; }
.me-demo-box {
width: 100%; height: 90px; border: 1.5px solid var(--line); border-radius: 8px; background: lightgray;
display: flex; align-items: center; justify-content: center; font-family: 'IBM Plex Mono', monospace; font-size: 0.78rem; color: #444;
transition: background-color 0.25s ease; cursor: pointer; user-select: none;
}
.me-click-box { width: 100%; height: 80px; border: 1.5px solid var(--line); border-radius: 8px; background: var(--paper-alt); display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.2rem; cursor: pointer; user-select: none; transition: border-color 0.2s ease, background 0.2s ease; }
.me-click-box.flash { border-color: var(--accent); background: var(--accent-soft); }
.me-click-label { font-family: 'IBM Plex Mono', monospace; font-size: 0.8rem; color: var(--ink); }
.me-click-counts { display: flex; gap: 1rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.64rem; color: var(--ink-soft); }
.me-move-box { width: 100%; height: 100px; border: 1.5px solid var(--line); border-radius: 8px; background: var(--paper-alt); display: flex; align-items: center; justify-content: center; font-family: 'IBM Plex Mono', monospace; font-size: 0.8rem; color: var(--ink-soft); position: relative; overflow: hidden; }
.me-move-dot { position: absolute; width: 8px; height: 8px; border-radius: 50%; background: var(--accent); opacity: 0; transform: translate(-50%, -50%); pointer-events: none; }
.me-bubble-outer { width: 100%; padding: 1.4rem; border: 1.5px solid var(--line); border-radius: 8px; background: var(--amber-soft); display: flex; align-items: center; justify-content: center; cursor: default; }
.me-bubble-inner { width: 70px; height: 70px; border: 1.5px solid var(--line); border-radius: 6px; background: var(--paper); display: flex; align-items: center; justify-content: center; font-family: 'IBM Plex Mono', monospace; font-size: 0.6rem; color: var(--ink-soft); text-align: center; }
.me-counters { display: flex; justify-content: center; gap: 1rem; margin-top: 0.9rem; flex-wrap: wrap; }
.me-counter { text-align: center; border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.4rem 0.8rem; border-radius: 6px; }
.me-counter-num { font-family: 'IBM Plex Mono', monospace; font-size: 1.1rem; font-weight: 700; color: var(--accent); }
.me-counter-label { font-family: 'IBM Plex Mono', monospace; font-size: 0.6rem; color: var(--ink-soft); }
.me-reset { display: block; margin: 0.9rem auto 0; border: 1px solid var(--line); background: var(--paper-alt); color: var(--ink-soft); padding: 0.35rem 0.8rem; font-size: 0.7rem; cursor: pointer; }
.me-reset:hover { border-color: var(--accent); color: var(--accent); }
.me-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.2rem; }
.me-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-mouseevents *, .infographic-mouseevents *::before, .infographic-mouseevents *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="me-wrap">
<div class="me-hero">
<h1>Mouse events Examples</h1>
<p>All real listeners below โ move your actual cursor over them.</p>
</div>
<div class="me-tabs">
<button class="me-tab is-active" data-tab="hover" type="button">hover</button>
<button class="me-tab" data-tab="click" type="button">click / dblclick</button>
<button class="me-tab" data-tab="move" type="button">mousemove</button>
<button class="me-tab" data-tab="bubble" type="button">over/out vs enter/leave</button>
</div>
<!-- hover -->
<div class="me-panel is-active" data-panel="hover">
<div class="me-code"><span class="tok-kw">const</span> <span class="tok-var">box</span> <span class="tok-punc">=</span> <span class="tok-var">document</span><span class="tok-punc">.</span><span class="tok-fn">getElementById</span><span class="tok-punc">(</span><span class="tok-str">"box"</span><span class="tok-punc">);</span>
<span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"mouseover"</span><span class="tok-punc">, () => {</span>
<span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-var">style</span><span class="tok-punc">.</span><span class="tok-var">backgroundColor</span> <span class="tok-punc">=</span> <span class="tok-str">"blue"</span><span class="tok-punc">;</span>
<span class="tok-punc">});</span>
<span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"mouseout"</span><span class="tok-punc">, () => {</span>
<span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-var">style</span><span class="tok-punc">.</span><span class="tok-var">backgroundColor</span> <span class="tok-punc">=</span> <span class="tok-str">"lightgray"</span><span class="tok-punc">;</span>
<span class="tok-punc">});</span></div>
<div class="me-demo-box" id="hoverBox">#box</div>
<p class="me-note">Hover in and out โ <strong>this box is running the exact code above.</strong></p>
</div>
<!-- click / dblclick -->
<div class="me-panel" data-panel="click">
<div class="me-click-box" id="clickBox">
<div class="me-click-label">click me</div>
</div>
<div class="me-counters">
<div class="me-counter"><div class="me-counter-num" id="clickCount">0</div><div class="me-counter-label">click</div></div>
<div class="me-counter"><div class="me-counter-num" id="dblclickCount">0</div><div class="me-counter-label">dblclick</div></div>
</div>
<button class="me-reset" id="clickReset" type="button">reset counters</button>
<p class="me-note">A <strong>dblclick</strong> also fires two separate <strong>click</strong> events first โ watch both counters move.</p>
</div>
<!-- mousemove -->
<div class="me-panel" data-panel="move">
<div class="me-move-box" id="moveBox">
<span id="moveCoords">move your cursor here</span>
<div class="me-move-dot" id="moveDot"></div>
</div>
<p class="me-note">Fires <strong>continuously</strong> โ every pixel the cursor travels inside the box.</p>
</div>
<!-- bubbling contrast -->
<div class="me-panel" data-panel="bubble">
<div class="me-bubble-outer" id="bubbleOuter">
<div class="me-bubble-inner" id="bubbleInner">inner box</div>
</div>
<div class="me-counters">
<div class="me-counter"><div class="me-counter-num" id="overCount">0</div><div class="me-counter-label">mouseover/out</div></div>
<div class="me-counter"><div class="me-counter-num" id="enterCount">0</div><div class="me-counter-label">mouseenter/leave</div></div>
</div>
<button class="me-reset" id="bubbleReset" type="button">reset counters</button>
<p class="me-note">Move across the outer box <strong>through</strong> the inner one โ <strong>over/out</strong> fires extra times crossing that boundary, <strong>enter/leave</strong> doesn't bubble and stays clean.</p>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-mouseevents')
|| document.querySelector('.infographic-mouseevents');
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');
wrapper.querySelectorAll('.me-tab').forEach(tab => {
tab.addEventListener('click', () => {
wrapper.querySelectorAll('.me-tab').forEach(t => t.classList.remove('is-active'));
wrapper.querySelectorAll('.me-panel').forEach(p => p.classList.remove('is-active'));
tab.classList.add('is-active');
wrapper.querySelector('.me-panel[data-panel="' + tab.dataset.tab + '"]').classList.add('is-active');
});
});
/* hover โ real mouseover/mouseout */
const hoverBox = wrapper.querySelector('#hoverBox');
hoverBox.addEventListener('mouseover', () => { hoverBox.style.backgroundColor = 'blue'; hoverBox.style.color = '#fff'; });
hoverBox.addEventListener('mouseout', () => { hoverBox.style.backgroundColor = 'lightgray'; hoverBox.style.color = '#444'; });
/* click / dblclick */
const clickBox = wrapper.querySelector('#clickBox');
const clickCount = wrapper.querySelector('#clickCount');
const dblclickCount = wrapper.querySelector('#dblclickCount');
let clicks = 0, dblclicks = 0;
clickBox.addEventListener('click', () => {
clicks++;
clickCount.textContent = clicks;
clickBox.classList.add('flash');
setTimeout(() => clickBox.classList.remove('flash'), 150);
});
clickBox.addEventListener('dblclick', () => {
dblclicks++;
dblclickCount.textContent = dblclicks;
});
wrapper.querySelector('#clickReset').addEventListener('click', () => {
clicks = 0; dblclicks = 0;
clickCount.textContent = '0';
dblclickCount.textContent = '0';
});
/* mousemove */
const moveBox = wrapper.querySelector('#moveBox');
const moveCoords = wrapper.querySelector('#moveCoords');
const moveDot = wrapper.querySelector('#moveDot');
moveBox.addEventListener('mousemove', (e) => {
const rect = moveBox.getBoundingClientRect();
const x = Math.round(e.clientX - rect.left);
const y = Math.round(e.clientY - rect.top);
moveCoords.textContent = 'x: ' + x + ', y: ' + y;
moveDot.style.opacity = '1';
moveDot.style.left = x + 'px';
moveDot.style.top = y + 'px';
});
moveBox.addEventListener('mouseleave', () => {
moveDot.style.opacity = '0';
moveCoords.textContent = 'move your cursor here';
});
/* over/out vs enter/leave bubbling contrast */
const bubbleOuter = wrapper.querySelector('#bubbleOuter');
const bubbleInner = wrapper.querySelector('#bubbleInner');
const overCount = wrapper.querySelector('#overCount');
const enterCount = wrapper.querySelector('#enterCount');
let overN = 0, enterN = 0;
bubbleOuter.addEventListener('mouseover', () => { overN++; overCount.textContent = overN; });
bubbleOuter.addEventListener('mouseout', () => { overN++; overCount.textContent = overN; });
bubbleOuter.addEventListener('mouseenter', () => { enterN++; enterCount.textContent = enterN; });
bubbleOuter.addEventListener('mouseleave', () => { enterN++; enterCount.textContent = enterN; });
wrapper.querySelector('#bubbleReset').addEventListener('click', () => {
overN = 0; enterN = 0;
overCount.textContent = '0';
enterCount.textContent = '0';
});
})();
</script>๐ง How it Works
mouseoverโ cursor enters โ color changesmouseoutโ cursor leaves โ color resets
๐ Real-World Uses
โ Hover effects
โ Tooltips
โ Dropdown menus
โ Drag & drop
โจ๏ธ Keyboard Events
Keyboard events track key presses and releases.
๐งฉ Common Keyboard Events
Event | Meaning |
|---|---|
| Key pressed |
| Key released |
๐งช Detect Pressed Key Example
document.addEventListener("keydown", (event) => {
console.log(event.key);
});a
Enter
Escape๐ง Important Properties
Property | Meaning |
|---|---|
| Actual key value |
| Physical key |
| Ctrl pressed? |
| Shift pressed? |
<div class="infographic-keyevents">
<style>
.infographic-keyevents {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-keyevents[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
}
.infographic-keyevents * { box-sizing: border-box; }
.infographic-keyevents h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-keyevents p { margin: 0; }
.infographic-keyevents button { font-family: inherit; }
.ke-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.ke-hero { text-align: center; margin-bottom: 1.2rem; }
.ke-hero h1 { font-size: clamp(1.3rem, 4.5vw, 1.8rem); }
.ke-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.84rem; }
.ke-tabs { display: flex; flex-wrap: wrap; gap: 0.35rem; justify-content: center; margin-bottom: 1.2rem; }
.ke-tab { border: 1px solid var(--line); background: var(--paper-alt); color: var(--ink-soft); padding: 0.34rem 0.6rem; font-size: 0.64rem; font-weight: 700; cursor: pointer; font-family: 'IBM Plex Mono', monospace; }
.ke-tab.is-active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.ke-tab:hover:not(.is-active) { border-color: var(--ink-soft); color: var(--ink); }
.ke-panel { display: none; }
.ke-panel.is-active { display: block; }
.ke-code { background: #1e1e1e; color: #d4d4d4; border-radius: 5px; padding: 0.6rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; line-height: 1.6; margin-bottom: 1rem; white-space: pre; overflow-x: auto; }
.tok-kw { color: #569cd6; padding: 0; margin: 0; }
.tok-fn { color: #dcdcaa; padding: 0; margin: 0; }
.tok-str { color: #ce9178; padding: 0; margin: 0; }
.tok-var { color: #9cdcfe; padding: 0; margin: 0; }
.tok-punc { color: #d4d4d4; padding: 0; margin: 0; }
.ke-focusbox {
width: 100%; min-height: 90px; border: 1.5px dashed var(--line); border-radius: 8px; background: var(--paper-alt);
display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.4rem; text-align: center;
outline: none; cursor: text; transition: border-color 0.25s ease, background 0.25s ease;
}
.ke-focusbox:focus { border-color: var(--accent); border-style: solid; background: var(--accent-soft); }
.ke-focus-hint { font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; color: var(--ink-soft); }
.ke-key-flash { font-family: 'IBM Plex Mono', monospace; font-size: 1.1rem; font-weight: 700; color: var(--accent); min-height: 1.6em; opacity: 0; transition: opacity 0.15s ease; }
.ke-key-flash.is-shown { opacity: 1; }
.ke-key-flash.up { color: var(--good); }
.ke-proptable { border: 1px solid var(--line); margin-top: 0.9rem; }
.ke-proprow { display: grid; grid-template-columns: 1fr 1fr; }
.ke-proprow + .ke-proprow { border-top: 1px solid var(--line); }
.ke-propkey { background: var(--paper-alt); padding: 0.4rem 0.6rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; color: var(--ink-soft); }
.ke-propval { padding: 0.4rem 0.6rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.68rem; color: var(--ink); border-left: 1px solid var(--line); transition: color 0.2s ease; }
.ke-propval.is-true { color: var(--good); font-weight: 700; }
.ke-toast {
display: flex; align-items: center; gap: 0.4rem; justify-content: center; margin-top: 0.9rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.74rem; font-weight: 700;
color: var(--good); border: 1.5px solid var(--good); background: var(--good-soft); padding: 0.4rem 0.7rem; border-radius: 5px; opacity: 0; transform: scale(0.9); transition: opacity 0.25s ease, transform 0.25s ease;
}
.ke-toast.is-shown { opacity: 1; transform: scale(1); }
.ke-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); margin-top: 1.2rem; }
.ke-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-keyevents *, .infographic-keyevents *::before, .infographic-keyevents *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="ke-wrap">
<div class="ke-hero">
<h1>Keyboard events Example</h1>
<p>Click a box below to focus it, then actually press keys.</p>
</div>
<div class="ke-tabs">
<button class="ke-tab is-active" data-tab="downup" type="button">keydown / keyup</button>
<button class="ke-tab" data-tab="props" type="button">key properties</button>
<button class="ke-tab" data-tab="shortcut" type="button">Ctrl + S shortcut</button>
</div>
<!-- keydown/keyup -->
<div class="ke-panel is-active" data-panel="downup">
<div class="ke-code"><span class="tok-var">document</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"keydown"</span><span class="tok-punc">, (</span><span class="tok-var">event</span><span class="tok-punc">) => {</span>
<span class="tok-var">console</span><span class="tok-punc">.</span><span class="tok-fn">log</span><span class="tok-punc">(</span><span class="tok-var">event</span><span class="tok-punc">.</span><span class="tok-var">key</span><span class="tok-punc">);</span>
<span class="tok-punc">});</span></div>
<div class="ke-focusbox" id="box1" tabindex="0">
<span class="ke-focus-hint">click here, then press any key</span>
<span class="ke-key-flash" id="flash1"></span>
</div>
<p class="ke-note">Press and hold a key โ <strong>keydown</strong> fires first, <strong>keyup</strong> fires the instant you let go.</p>
</div>
<!-- key properties -->
<div class="ke-panel" data-panel="props">
<div class="ke-focusbox" id="box2" tabindex="0">
<span class="ke-focus-hint" id="hint2">click here, then press a key (try holding Ctrl or Shift)</span>
<span class="ke-key-flash" id="flash2"></span>
</div>
<div class="ke-proptable">
<div class="ke-proprow"><div class="ke-propkey">event.key</div><div class="ke-propval" id="pKey">โ</div></div>
<div class="ke-proprow"><div class="ke-propkey">event.code</div><div class="ke-propval" id="pCode">โ</div></div>
<div class="ke-proprow"><div class="ke-propkey">event.ctrlKey</div><div class="ke-propval" id="pCtrl">false</div></div>
<div class="ke-proprow"><div class="ke-propkey">event.shiftKey</div><div class="ke-propval" id="pShift">false</div></div>
</div>
<p class="ke-note"><strong>key</strong> is what the key produces, <strong>code</strong> is which physical key it is โ they can differ.</p>
</div>
<!-- shortcut -->
<div class="ke-panel" data-panel="shortcut">
<div class="ke-code"><span class="tok-var">document</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"keydown"</span><span class="tok-punc">, (</span><span class="tok-var">event</span><span class="tok-punc">) => {</span>
<span class="tok-kw">if</span> <span class="tok-punc">(</span><span class="tok-var">event</span><span class="tok-punc">.</span><span class="tok-var">ctrlKey</span> <span class="tok-punc">&&</span> <span class="tok-var">event</span><span class="tok-punc">.</span><span class="tok-var">key</span> <span class="tok-punc">===</span> <span class="tok-str">"s"</span><span class="tok-punc">) {</span>
<span class="tok-var">event</span><span class="tok-punc">.</span><span class="tok-fn">preventDefault</span><span class="tok-punc">();</span>
<span class="tok-var">console</span><span class="tok-punc">.</span><span class="tok-fn">log</span><span class="tok-punc">(</span><span class="tok-str">"Save shortcut triggered"</span><span class="tok-punc">);</span>
<span class="tok-punc">}</span>
<span class="tok-punc">});</span></div>
<div class="ke-focusbox" id="box3" tabindex="0">
<span class="ke-focus-hint">click here, then press Ctrl + S</span>
</div>
<div class="ke-toast" id="toast3">โ Save shortcut triggered โ browser's save dialog blocked</div>
<p class="ke-note">Without <strong>preventDefault()</strong>, the browser's own "Save Page" dialog would pop up instead.</p>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-keyevents')
|| document.querySelector('.infographic-keyevents');
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');
wrapper.querySelectorAll('.ke-tab').forEach(tab => {
tab.addEventListener('click', () => {
wrapper.querySelectorAll('.ke-tab').forEach(t => t.classList.remove('is-active'));
wrapper.querySelectorAll('.ke-panel').forEach(p => p.classList.remove('is-active'));
tab.classList.add('is-active');
wrapper.querySelector('.ke-panel[data-panel="' + tab.dataset.tab + '"]').classList.add('is-active');
});
});
/* keydown/keyup */
const box1 = wrapper.querySelector('#box1');
const flash1 = wrapper.querySelector('#flash1');
box1.addEventListener('keydown', (e) => {
flash1.textContent = e.key + ' โ';
flash1.classList.remove('up');
flash1.classList.add('is-shown');
});
box1.addEventListener('keyup', (e) => {
flash1.textContent = e.key + ' โ';
flash1.classList.add('up', 'is-shown');
});
/* key properties */
const box2 = wrapper.querySelector('#box2');
const hint2 = wrapper.querySelector('#hint2');
const flash2 = wrapper.querySelector('#flash2');
const pKey = wrapper.querySelector('#pKey');
const pCode = wrapper.querySelector('#pCode');
const pCtrl = wrapper.querySelector('#pCtrl');
const pShift = wrapper.querySelector('#pShift');
box2.addEventListener('keydown', (e) => {
hint2.style.display = 'none';
flash2.textContent = e.key;
flash2.classList.add('is-shown');
pKey.textContent = '"' + e.key + '"';
pCode.textContent = '"' + e.code + '"';
pCtrl.textContent = e.ctrlKey ? 'true' : 'false';
pCtrl.classList.toggle('is-true', e.ctrlKey);
pShift.textContent = e.shiftKey ? 'true' : 'false';
pShift.classList.toggle('is-true', e.shiftKey);
});
/* Ctrl+S shortcut */
const box3 = wrapper.querySelector('#box3');
const toast3 = wrapper.querySelector('#toast3');
box3.addEventListener('keydown', (e) => {
if (e.ctrlKey && e.key.toLowerCase() === 's') {
e.preventDefault();
toast3.classList.remove('is-shown');
void toast3.offsetWidth;
toast3.classList.add('is-shown');
}
});
})();
</script>๐ Real-World Uses
โ Form validation
โ Keyboard shortcuts
โ Games
โ Accessibility
๐ Scroll Events
Scroll events fire when the user scrolls the page or an element.
<div class="infographic-scrollevents">
<style>
.infographic-scrollevents {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
--bad: #c8452e;
--bad-soft: #f7e3df;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-scrollevents[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
--bad: #e0715a;
--bad-soft: #2e211d;
}
.infographic-scrollevents * { box-sizing: border-box; }
.infographic-scrollevents h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-scrollevents p { margin: 0; }
.infographic-scrollevents button { font-family: inherit; }
.se-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.se-hero { text-align: center; margin-bottom: 1.2rem; }
.se-hero h1 { font-size: clamp(1.3rem, 4.5vw, 1.8rem); }
.se-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.84rem; }
.se-tabs { display: flex; flex-wrap: wrap; gap: 0.35rem; justify-content: center; margin-bottom: 1.2rem; }
.se-tab { border: 1px solid var(--line); background: var(--paper-alt); color: var(--ink-soft); padding: 0.34rem 0.6rem; font-size: 0.64rem; font-weight: 700; cursor: pointer; font-family: 'IBM Plex Mono', monospace; }
.se-tab.is-active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.se-tab:hover:not(.is-active) { border-color: var(--ink-soft); color: var(--ink); }
.se-panel { display: none; }
.se-panel.is-active { display: block; }
.se-code { background: #1e1e1e; color: #d4d4d4; border-radius: 5px; padding: 0.6rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; line-height: 1.6; margin-bottom: 1rem; white-space: pre; overflow-x: auto; }
.tok-kw { color: #569cd6; padding: 0; margin: 0; }
.tok-fn { color: #dcdcaa; padding: 0; margin: 0; }
.tok-str { color: #ce9178; padding: 0; margin: 0; }
.tok-var { color: #9cdcfe; padding: 0; margin: 0; }
.tok-punc { color: #d4d4d4; padding: 0; margin: 0; }
/* scrollable demo box */
.se-scrollbox { height: 120px; overflow-y: auto; border: 1.5px solid var(--line); border-radius: 8px; background: var(--paper-alt); }
.se-scroll-inner { padding: 1rem; }
.se-scroll-line { height: 8px; background: var(--paper); border: 1px solid var(--line); border-radius: 3px; margin-bottom: 0.5rem; }
.se-scroll-line:nth-child(odd) { width: 85%; }
.se-scroll-line:nth-child(even) { width: 65%; }
.se-readout { display: flex; justify-content: center; gap: 1rem; margin-top: 0.9rem; flex-wrap: wrap; }
.se-readout-item { text-align: center; border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.4rem 0.8rem; border-radius: 6px; }
.se-readout-num { font-family: 'IBM Plex Mono', monospace; font-size: 1rem; font-weight: 700; color: var(--accent); }
.se-readout-label { font-family: 'IBM Plex Mono', monospace; font-size: 0.58rem; color: var(--ink-soft); }
/* progress bar demo */
.se-progress-track { height: 6px; background: var(--paper-alt); border: 1px solid var(--line); margin-bottom: 0.7rem; overflow: hidden; }
.se-progress-fill { height: 100%; width: 0%; background: var(--good); transition: width 0.05s linear; }
/* throttle demo */
.se-throttle-readout { display: flex; justify-content: center; gap: 1rem; margin-top: 0.9rem; }
.se-reset { display: block; margin: 0.9rem auto 0; border: 1px solid var(--line); background: var(--paper-alt); color: var(--ink-soft); padding: 0.35rem 0.8rem; font-size: 0.7rem; cursor: pointer; }
.se-reset:hover { border-color: var(--accent); color: var(--accent); }
.se-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.2rem; }
.se-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-scrollevents *, .infographic-scrollevents *::before, .infographic-scrollevents *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="se-wrap">
<div class="se-tabs">
<button class="se-tab is-active" data-tab="basic" type="button">the event</button>
<button class="se-tab" data-tab="progress" type="button">progress bar</button>
<button class="se-tab" data-tab="throttle" type="button">why you throttle</button>
</div>
<!-- basic -->
<div class="se-panel is-active" data-panel="basic">
<div class="se-code"><span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"scroll"</span><span class="tok-punc">, () => {</span>
<span class="tok-var">console</span><span class="tok-punc">.</span><span class="tok-fn">log</span><span class="tok-punc">(</span><span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-var">scrollTop</span><span class="tok-punc">);</span>
<span class="tok-punc">});</span></div>
<div class="se-scrollbox" id="basicBox">
<div class="se-scroll-inner">
<div class="se-scroll-line"></div><div class="se-scroll-line"></div><div class="se-scroll-line"></div>
<div class="se-scroll-line"></div><div class="se-scroll-line"></div><div class="se-scroll-line"></div>
<div class="se-scroll-line"></div><div class="se-scroll-line"></div><div class="se-scroll-line"></div>
<div class="se-scroll-line"></div>
</div>
</div>
<div class="se-readout">
<div class="se-readout-item"><div class="se-readout-num" id="scrollTopVal">0</div><div class="se-readout-label">scrollTop</div></div>
<div class="se-readout-item"><div class="se-readout-num" id="fireCount">0</div><div class="se-readout-label">times fired</div></div>
</div>
<p class="se-note">Fires <strong>continuously</strong> as you scroll โ watch the fire count climb fast for a small scroll.</p>
</div>
<!-- progress bar -->
<div class="se-panel" data-panel="progress">
<div class="se-code"><span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"scroll"</span><span class="tok-punc">, () => {</span>
<span class="tok-kw">const</span> <span class="tok-var">pct</span> <span class="tok-punc">=</span> <span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-var">scrollTop</span> <span class="tok-punc">/</span> <span class="tok-punc">(</span><span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-var">scrollHeight</span> <span class="tok-punc">-</span> <span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-var">clientHeight</span><span class="tok-punc">);</span>
<span class="tok-var">bar</span><span class="tok-punc">.</span><span class="tok-var">style</span><span class="tok-punc">.</span><span class="tok-var">width</span> <span class="tok-punc">=</span> <span class="tok-punc">(</span><span class="tok-var">pct</span> <span class="tok-punc">*</span> <span class="tok-num">100</span><span class="tok-punc">) + </span><span class="tok-str">"%"</span><span class="tok-punc">;</span>
<span class="tok-punc">});</span></div>
<div class="se-progress-track"><div class="se-progress-fill" id="progressFill"></div></div>
<div class="se-scrollbox" id="progressBox">
<div class="se-scroll-inner">
<div class="se-scroll-line"></div><div class="se-scroll-line"></div><div class="se-scroll-line"></div>
<div class="se-scroll-line"></div><div class="se-scroll-line"></div><div class="se-scroll-line"></div>
<div class="se-scroll-line"></div><div class="se-scroll-line"></div><div class="se-scroll-line"></div>
<div class="se-scroll-line"></div>
</div>
</div>
<p class="se-note">Reading-progress bars, back-to-top buttons, sticky headers โ all built on this same scroll-position math.</p>
</div>
<!-- throttle -->
<div class="se-panel" data-panel="throttle">
<div class="se-code"><span class="tok-kw">let</span> <span class="tok-var">ticking</span> <span class="tok-punc">=</span> <span class="tok-kw">false</span><span class="tok-punc">;</span>
<span class="tok-var">box</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"scroll"</span><span class="tok-punc">, () => {</span>
<span class="tok-kw">if</span> <span class="tok-punc">(</span><span class="tok-var">ticking</span><span class="tok-punc">)</span> <span class="tok-kw">return</span><span class="tok-punc">;</span>
<span class="tok-var">ticking</span> <span class="tok-punc">=</span> <span class="tok-kw">true</span><span class="tok-punc">;</span>
<span class="tok-fn">requestAnimationFrame</span><span class="tok-punc">(() => {</span>
<span class="tok-com">// do the expensive work here</span>
<span class="tok-var">ticking</span> <span class="tok-punc">=</span> <span class="tok-kw">false</span><span class="tok-punc">;</span>
<span class="tok-punc">});</span>
<span class="tok-punc">});</span></div>
<div class="se-scrollbox" id="throttleBox">
<div class="se-scroll-inner">
<div class="se-scroll-line"></div><div class="se-scroll-line"></div><div class="se-scroll-line"></div>
<div class="se-scroll-line"></div><div class="se-scroll-line"></div><div class="se-scroll-line"></div>
<div class="se-scroll-line"></div><div class="se-scroll-line"></div><div class="se-scroll-line"></div>
<div class="se-scroll-line"></div>
</div>
</div>
<div class="se-throttle-readout">
<div class="se-readout-item"><div class="se-readout-num" id="rawCount">0</div><div class="se-readout-label">raw fires</div></div>
<div class="se-readout-item"><div class="se-readout-num" id="throttledCount">0</div><div class="se-readout-label">throttled runs</div></div>
</div>
<button class="se-reset" id="throttleReset" type="button">reset counters</button>
<p class="se-note">The raw event fires <strong>way</strong> more often than any real work needs to run โ that's why heavy scroll logic gets throttled to once per frame.</p>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-scrollevents')
|| document.querySelector('.infographic-scrollevents');
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');
wrapper.querySelectorAll('.se-tab').forEach(tab => {
tab.addEventListener('click', () => {
wrapper.querySelectorAll('.se-tab').forEach(t => t.classList.remove('is-active'));
wrapper.querySelectorAll('.se-panel').forEach(p => p.classList.remove('is-active'));
tab.classList.add('is-active');
wrapper.querySelector('.se-panel[data-panel="' + tab.dataset.tab + '"]').classList.add('is-active');
});
});
/* basic */
const basicBox = wrapper.querySelector('#basicBox');
const scrollTopVal = wrapper.querySelector('#scrollTopVal');
const fireCount = wrapper.querySelector('#fireCount');
let fires = 0;
basicBox.addEventListener('scroll', () => {
scrollTopVal.textContent = Math.round(basicBox.scrollTop);
fires++;
fireCount.textContent = fires;
});
/* progress */
const progressBox = wrapper.querySelector('#progressBox');
const progressFill = wrapper.querySelector('#progressFill');
progressBox.addEventListener('scroll', () => {
const max = progressBox.scrollHeight - progressBox.clientHeight;
const pct = max > 0 ? (progressBox.scrollTop / max) * 100 : 0;
progressFill.style.width = pct + '%';
});
/* throttle */
const throttleBox = wrapper.querySelector('#throttleBox');
const rawCount = wrapper.querySelector('#rawCount');
const throttledCount = wrapper.querySelector('#throttledCount');
let raw = 0, throttled = 0, ticking = false;
throttleBox.addEventListener('scroll', () => {
raw++;
rawCount.textContent = raw;
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
throttled++;
throttledCount.textContent = throttled;
ticking = false;
});
});
wrapper.querySelector('#throttleReset').addEventListener('click', () => {
raw = 0; throttled = 0;
rawCount.textContent = '0';
throttledCount.textContent = '0';
});
})();
</script>๐ Common Use Cases
โ Infinite scrolling
โ Sticky headers
โ Reveal animations
โ Scroll progress bars
โ ๏ธ Performance Warning
Fires many times per second
Heavy logic can cause lag
โ Use throttle or debounce in real apps
๐ฏ scrollIntoView()
Scrolls a specific element into the visible area.
<div class="infographic-scrollintoview">
<style>
.infographic-scrollintoview {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-scrollintoview[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
}
.infographic-scrollintoview * { box-sizing: border-box; }
.infographic-scrollintoview h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-scrollintoview p { margin: 0; }
.infographic-scrollintoview button { font-family: inherit; }
.siv-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.siv-hero { text-align: center; margin-bottom: 1.2rem; }
.siv-hero h1 { font-size: clamp(1.3rem, 4.5vw, 1.8rem); }
.siv-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.84rem; }
.siv-code { background: #1e1e1e; color: #d4d4d4; border-radius: 5px; padding: 0.6rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; line-height: 1.6; margin-bottom: 1rem; white-space: pre; overflow-x: auto; }
.tok-kw { color: #569cd6; padding: 0; margin: 0; }
.tok-fn { color: #dcdcaa; padding: 0; margin: 0; }
.tok-str { color: #ce9178; padding: 0; margin: 0; }
.tok-var { color: #9cdcfe; padding: 0; margin: 0; }
.tok-punc { color: #d4d4d4; padding: 0; margin: 0; }
.tok-prop { color: #9cdcfe; padding: 0; margin: 0; }
.siv-scrollbox { height: 150px; overflow-y: auto; border: 1.5px solid var(--line); border-radius: 8px; background: var(--paper-alt); margin-bottom: 1rem; scroll-behavior: smooth; }
.siv-section {
height: 90px; margin: 0.6rem; border: 1.5px solid var(--line); border-radius: 6px; background: var(--paper);
display: flex; align-items: center; justify-content: center; font-family: 'IBM Plex Mono', monospace; font-size: 0.9rem; color: var(--ink-soft);
transition: border-color 0.4s ease, background 0.4s ease, color 0.4s ease;
}
.siv-section.is-target { border-color: var(--good); background: var(--good-soft); color: var(--good); font-weight: 700; }
.siv-btnrow { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; margin-bottom: 1rem; }
.siv-btn { border: 1.5px solid var(--line); background: var(--paper-alt); color: var(--ink); padding: 0.4rem 0.8rem; font-size: 0.74rem; cursor: pointer; border-radius: 5px; }
.siv-btn:hover { border-color: var(--accent); color: var(--accent); }
.siv-btn.primary { border: none; background: var(--accent); color: #fff; font-weight: 600; }
.siv-toggle-row { display: flex; align-items: center; justify-content: center; gap: 0.6rem; margin-bottom: 0.6rem; }
.siv-toggle-label { font-family: 'IBM Plex Mono', monospace; font-size: 0.7rem; color: var(--ink-soft); }
.siv-toggle { display: flex; border: 1.5px solid var(--line); overflow: hidden; }
.siv-toggle button { border: none; background: var(--paper-alt); color: var(--ink-soft); padding: 0.3rem 0.6rem; font-size: 0.66rem; font-weight: 700; cursor: pointer; font-family: 'IBM Plex Mono', monospace; }
.siv-toggle button.is-active { background: var(--accent-soft); color: var(--accent); }
.siv-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); margin-top: 1.2rem; }
.siv-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-scrollintoview *, .infographic-scrollintoview *::before, .infographic-scrollintoview *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="siv-wrap">
<div class="siv-code"><span class="tok-var">document</span><span class="tok-punc">.</span><span class="tok-fn">getElementById</span><span class="tok-punc">(</span><span class="tok-str">"section-4"</span><span class="tok-punc">).</span><span class="tok-fn">scrollIntoView</span><span class="tok-punc">({</span>
<span class="tok-prop">behavior</span><span class="tok-punc">:</span> <span class="tok-str">"smooth"</span><span class="tok-punc">,</span>
<span class="tok-prop">block</span><span class="tok-punc">:</span> <span class="tok-str">"center"</span>
<span class="tok-punc">});</span></div>
<div class="siv-scrollbox" id="scrollBox">
<div class="siv-section" id="sec1">Section 1</div>
<div class="siv-section" id="sec2">Section 2</div>
<div class="siv-section" id="sec3">Section 3</div>
<div class="siv-section" id="sec4">Section 4</div>
<div class="siv-section" id="sec5">Section 5</div>
<div class="siv-section" id="sec6">Section 6</div>
</div>
<div class="siv-toggle-row">
<span class="siv-toggle-label">block:</span>
<div class="siv-toggle" id="blockToggle">
<button class="is-active" data-val="start" type="button">start</button>
<button data-val="center" type="button">center</button>
<button data-val="end" type="button">end</button>
</div>
</div>
<div class="siv-btnrow">
<button class="siv-btn primary" data-target="sec1" type="button">Section 1</button>
<button class="siv-btn primary" data-target="sec3" type="button">Section 3</button>
<button class="siv-btn primary" data-target="sec6" type="button">Section 6</button>
</div>
<p class="siv-note"><strong>block</strong> decides where the target lands in the box โ top, middle, or bottom โ try switching it and clicking the same section again.</p>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-scrollintoview')
|| document.querySelector('.infographic-scrollintoview');
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');
let blockMode = 'start';
const blockToggle = wrapper.querySelector('#blockToggle');
blockToggle.querySelectorAll('button').forEach(b => {
b.addEventListener('click', () => {
blockMode = b.dataset.val;
blockToggle.querySelectorAll('button').forEach(x => x.classList.remove('is-active'));
b.classList.add('is-active');
});
});
wrapper.querySelectorAll('.siv-btn').forEach(btn => {
btn.addEventListener('click', () => {
const target = wrapper.querySelector('#' + btn.dataset.target);
wrapper.querySelectorAll('.siv-section').forEach(s => s.classList.remove('is-target'));
target.classList.add('is-target');
target.scrollIntoView({ behavior: 'smooth', block: blockMode });
});
});
})();
</script>๐ง Why Itโs Useful
โ No manual scroll math
โ Cleaner code
โ Smooth UX
๐ Common Uses
โ Navigation links
โ โScroll to sectionโ buttons
โ Help & FAQ navigation
๐ฏ Focus Events
Focus events detect when an element gains or loses focus.
Event | Meaning |
|---|---|
| Element selected |
| Element deselected |
| Elements focused in |
| Elemenrs Focused out |
<div class="infographic-focusevents">
<style>
.infographic-focusevents {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
--bad: #c8452e;
--bad-soft: #f7e3df;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-focusevents[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
--bad: #e0715a;
--bad-soft: #2e211d;
}
.infographic-focusevents * { box-sizing: border-box; }
.infographic-focusevents h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-focusevents p { margin: 0; }
.infographic-focusevents button { font-family: inherit; }
.fe-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.fe-hero { text-align: center; margin-bottom: 1.2rem; }
.fe-hero h1 { font-size: clamp(1.3rem, 4.5vw, 1.8rem); }
.fe-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.84rem; }
.fe-tabs { display: flex; flex-wrap: wrap; gap: 0.35rem; justify-content: center; margin-bottom: 1.2rem; }
.fe-tab { border: 1px solid var(--line); background: var(--paper-alt); color: var(--ink-soft); padding: 0.34rem 0.6rem; font-size: 0.64rem; font-weight: 700; cursor: pointer; font-family: 'IBM Plex Mono', monospace; }
.fe-tab.is-active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); }
.fe-tab:hover:not(.is-active) { border-color: var(--ink-soft); color: var(--ink); }
.fe-panel { display: none; }
.fe-panel.is-active { display: block; }
.fe-code { background: #1e1e1e; color: #d4d4d4; border-radius: 5px; padding: 0.6rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; line-height: 1.6; margin-bottom: 1rem; white-space: pre; overflow-x: auto; }
.tok-kw { color: #569cd6; padding: 0; margin: 0; }
.tok-fn { color: #dcdcaa; padding: 0; margin: 0; }
.tok-str { color: #ce9178; padding: 0; margin: 0; }
.tok-var { color: #9cdcfe; padding: 0; margin: 0; }
.tok-punc { color: #d4d4d4; padding: 0; margin: 0; }
.fe-field-label { font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; color: var(--ink-soft); margin-bottom: 0.3rem; }
.fe-input { width: 100%; border: 1.5px solid var(--line); border-radius: 5px; padding: 0.5rem 0.7rem; font-size: 0.85rem; color: var(--ink); background: var(--paper); font-family: inherit; transition: border-color 0.25s ease, background 0.25s ease; }
.fe-input:focus { outline: none; border-color: var(--accent); background: var(--accent-soft); }
.fe-input.is-invalid { border-color: var(--bad); background: var(--bad-soft); }
.fe-validation { font-family: 'IBM Plex Mono', monospace; font-size: 0.68rem; margin-top: 0.4rem; min-height: 1.4em; color: var(--bad); opacity: 0; transition: opacity 0.25s ease; }
.fe-validation.is-shown { opacity: 1; }
.fe-status { text-align: center; font-family: 'IBM Plex Mono', monospace; font-size: 0.68rem; color: var(--ink-soft); margin-top: 0.9rem; }
.fe-status strong { color: var(--accent); }
/* focusin/focusout delegation demo */
.fe-form { border: 1.5px solid var(--line); border-radius: 8px; padding: 0.9rem; background: var(--paper-alt); transition: border-color 0.3s ease, background 0.3s ease; }
.fe-form.is-active-form { border-color: var(--good); background: var(--good-soft); }
.fe-form-listen { font-family: 'IBM Plex Mono', monospace; font-size: 0.6rem; color: var(--ink-soft); margin-bottom: 0.6rem; text-align: center; }
.fe-form-field { margin-bottom: 0.6rem; }
.fe-form-field:last-child { margin-bottom: 0; }
.fe-counter-row { display: flex; justify-content: center; gap: 1rem; margin-top: 0.9rem; }
.fe-counter { text-align: center; border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.4rem 0.8rem; border-radius: 6px; }
.fe-counter-num { font-family: 'IBM Plex Mono', monospace; font-size: 1.05rem; font-weight: 700; color: var(--accent); }
.fe-counter-label { font-family: 'IBM Plex Mono', monospace; font-size: 0.58rem; color: var(--ink-soft); }
.fe-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1rem; }
.fe-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-focusevents *, .infographic-focusevents *::before, .infographic-focusevents *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="fe-wrap">
<div class="fe-tabs">
<button class="fe-tab is-active" data-tab="basic" type="button">focus / blur</button>
<button class="fe-tab" data-tab="delegate" type="button">focusin / focusout</button>
</div>
<!-- focus/blur -->
<div class="fe-panel is-active" data-panel="basic">
<div class="fe-code"><span class="tok-var">input</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"focus"</span><span class="tok-punc">, () => {</span>
<span class="tok-var">input</span><span class="tok-punc">.</span><span class="tok-var">style</span><span class="tok-punc">.</span><span class="tok-var">borderColor</span> <span class="tok-punc">=</span> <span class="tok-str">"blue"</span><span class="tok-punc">;</span>
<span class="tok-punc">});</span>
<span class="tok-var">input</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"blur"</span><span class="tok-punc">, () => {</span>
<span class="tok-kw">if</span> <span class="tok-punc">(!</span><span class="tok-var">input</span><span class="tok-punc">.</span><span class="tok-var">value</span><span class="tok-punc">) {</span>
<span class="tok-var">showError</span><span class="tok-punc">(</span><span class="tok-str">"This field is required"</span><span class="tok-punc">);</span>
<span class="tok-punc">}</span>
<span class="tok-punc">});</span></div>
<div class="fe-field-label">Email</div>
<input class="fe-input" id="emailInput" type="text" placeholder="you@example.com">
<div class="fe-validation" id="emailError">This field is required</div>
<div class="fe-status" id="basicStatus">not focused</div>
<p class="fe-note"><strong>focus</strong> fires the moment it's clicked into, <strong>blur</strong> the moment it loses focus โ neither one bubbles.</p>
</div>
<!-- focusin/focusout -->
<div class="fe-panel" data-panel="delegate">
<div class="fe-code"><span class="tok-var">form</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"focusin"</span><span class="tok-punc">, (</span><span class="tok-var">event</span><span class="tok-punc">) => {</span>
<span class="tok-var">console</span><span class="tok-punc">.</span><span class="tok-fn">log</span><span class="tok-punc">(</span><span class="tok-var">event</span><span class="tok-punc">.</span><span class="tok-var">target</span><span class="tok-punc">.</span><span class="tok-var">name</span> <span class="tok-punc">+</span> <span class="tok-str">" focused"</span><span class="tok-punc">);</span>
<span class="tok-punc">});</span>
<span class="tok-com">// one listener on the form โ not one per field</span></div>
<div class="fe-form" id="delegateForm">
<div class="fe-form-listen">single listener โ on the form itself</div>
<div class="fe-form-field"><input class="fe-input" name="name" type="text" placeholder="Name"></div>
<div class="fe-form-field"><input class="fe-input" name="email" type="text" placeholder="Email"></div>
<div class="fe-form-field"><input class="fe-input" name="phone" type="text" placeholder="Phone"></div>
</div>
<div class="fe-counter-row">
<div class="fe-counter"><div class="fe-counter-num" id="focusinCount">0</div><div class="fe-counter-label">focusin</div></div>
<div class="fe-counter"><div class="fe-counter-num" id="focusoutCount">0</div><div class="fe-counter-label">focusout</div></div>
</div>
<p class="fe-note">Tab or click between the three fields โ <strong>one listener on the form</strong> catches every field's focus change, because <strong>focusin/focusout</strong> bubble.</p>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-focusevents')
|| document.querySelector('.infographic-focusevents');
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');
wrapper.querySelectorAll('.fe-tab').forEach(tab => {
tab.addEventListener('click', () => {
wrapper.querySelectorAll('.fe-tab').forEach(t => t.classList.remove('is-active'));
wrapper.querySelectorAll('.fe-panel').forEach(p => p.classList.remove('is-active'));
tab.classList.add('is-active');
wrapper.querySelector('.fe-panel[data-panel="' + tab.dataset.tab + '"]').classList.add('is-active');
});
});
/* focus/blur */
const emailInput = wrapper.querySelector('#emailInput');
const emailError = wrapper.querySelector('#emailError');
const basicStatus = wrapper.querySelector('#basicStatus');
emailInput.addEventListener('focus', () => {
basicStatus.innerHTML = '<strong>focus</strong> fired';
emailInput.classList.remove('is-invalid');
emailError.classList.remove('is-shown');
});
emailInput.addEventListener('blur', () => {
basicStatus.innerHTML = '<strong>blur</strong> fired';
if (!emailInput.value.trim()) {
emailInput.classList.add('is-invalid');
emailError.classList.add('is-shown');
}
});
/* focusin/focusout */
const delegateForm = wrapper.querySelector('#delegateForm');
const focusinCount = wrapper.querySelector('#focusinCount');
const focusoutCount = wrapper.querySelector('#focusoutCount');
let inN = 0, outN = 0;
delegateForm.addEventListener('focusin', () => {
inN++;
focusinCount.textContent = inN;
delegateForm.classList.add('is-active-form');
});
delegateForm.addEventListener('focusout', () => {
outN++;
focusoutCount.textContent = outN;
delegateForm.classList.remove('is-active-form');
});
})();
</script>๐ Why Focus Events Matter
โ Form validation
โ Accessibility (keyboard users)
โ Better UX feedback
๐ hashchange Event
Triggered when the URL hash (#) changes.
<div class="infographic-hashchange">
<style>
.infographic-hashchange {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--good-soft: #e1f2ea;
box-sizing: border-box;
max-width: 880px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-hashchange[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--good-soft: #16281f;
}
.infographic-hashchange * { box-sizing: border-box; }
.infographic-hashchange h1 { font-family: 'Fraunces', Georgia, serif; font-weight: 600; margin: 0; }
.infographic-hashchange p { margin: 0; }
.infographic-hashchange button, .infographic-hashchange a { font-family: inherit; }
.hc-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.hc-hero { text-align: center; margin-bottom: 1.2rem; }
.hc-hero h1 { font-size: clamp(1.3rem, 4.5vw, 1.8rem); }
.hc-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.84rem; }
.hc-code { background: #1e1e1e; color: #d4d4d4; border-radius: 5px; padding: 0.6rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; line-height: 1.6; margin-bottom: 1.1rem; white-space: pre; overflow-x: auto; }
.tok-kw { color: #569cd6; padding: 0; margin: 0; }
.tok-fn { color: #dcdcaa; padding: 0; margin: 0; }
.tok-str { color: #ce9178; padding: 0; margin: 0; }
.tok-var { color: #9cdcfe; padding: 0; margin: 0; }
.tok-punc { color: #d4d4d4; padding: 0; margin: 0; }
.hc-nav { display: flex; justify-content: center; gap: 0.5rem; margin-bottom: 1rem; flex-wrap: wrap; }
.hc-navlink { border: 1.5px solid var(--line); background: var(--paper-alt); color: var(--ink); padding: 0.4rem 0.9rem; font-size: 0.78rem; cursor: pointer; border-radius: 5px; text-decoration: none; transition: border-color 0.25s ease, background 0.25s ease, color 0.25s ease; }
.hc-navlink:hover { border-color: var(--accent); color: var(--accent); }
.hc-navlink.is-active { border-color: var(--accent); background: var(--accent-soft); color: var(--accent); font-weight: 600; }
.hc-urlbar { font-family: 'IBM Plex Mono', monospace; font-size: 0.7rem; text-align: center; color: var(--ink-soft); border: 1px solid var(--line); background: var(--paper-alt); padding: 0.4rem 0.7rem; border-radius: 5px; margin-bottom: 1rem; overflow-x: auto; white-space: nowrap; }
.hc-urlbar strong { color: var(--accent); }
.hc-page { border: 1.5px solid var(--line); background: var(--paper-alt); border-radius: 6px; overflow: hidden; margin-bottom: 1rem; }
.hc-page-bar { background: #2b2b2b; padding: 0.4rem 0.6rem; display: flex; gap: 0.3rem; }
.hc-page-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }
.hc-page-body { padding: 1.2rem; background: var(--paper); text-align: center; min-height: 60px; display: flex; align-items: center; justify-content: center; }
.hc-page-content { font-family: 'IBM Plex Mono', monospace; font-size: 0.9rem; color: var(--ink); opacity: 0; transition: opacity 0.25s ease; }
.hc-page-content.is-shown { opacity: 1; }
.hc-console { margin: 0 auto; max-width: 340px; text-align: left; border: 1.5px solid var(--line); background: var(--paper-alt); padding: 0.55rem 0.8rem; font-family: 'IBM Plex Mono', monospace; font-size: 0.66rem; min-height: 2.6em; line-height: 1.6; }
.hc-console .hc-cline { display: block; color: var(--good); opacity: 0; transition: opacity 0.25s ease; word-break: break-all; }
.hc-console .hc-cline.is-shown { opacity: 1; }
.hc-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.2rem; }
.hc-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-hashchange *, .infographic-hashchange *::before, .infographic-hashchange *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="hc-wrap">
<div class="hc-code"><span class="tok-var">window</span><span class="tok-punc">.</span><span class="tok-fn">addEventListener</span><span class="tok-punc">(</span><span class="tok-str">"hashchange"</span><span class="tok-punc">, (</span><span class="tok-var">event</span><span class="tok-punc">) => {</span>
<span class="tok-var">console</span><span class="tok-punc">.</span><span class="tok-fn">log</span><span class="tok-punc">(</span><span class="tok-str">"from:"</span><span class="tok-punc">,</span> <span class="tok-var">event</span><span class="tok-punc">.</span><span class="tok-var">oldURL</span><span class="tok-punc">);</span>
<span class="tok-var">console</span><span class="tok-punc">.</span><span class="tok-fn">log</span><span class="tok-punc">(</span><span class="tok-str">"to:"</span><span class="tok-punc">,</span> <span class="tok-var">event</span><span class="tok-punc">.</span><span class="tok-var">newURL</span><span class="tok-punc">);</span>
<span class="tok-var">render</span><span class="tok-punc">(</span><span class="tok-var">location</span><span class="tok-punc">.</span><span class="tok-var">hash</span><span class="tok-punc">);</span>
<span class="tok-punc">});</span></div>
<div class="hc-nav">
<a class="hc-navlink" data-hash="#home" href="#home">Home</a>
<a class="hc-navlink" data-hash="#about" href="#about">About</a>
<a class="hc-navlink" data-hash="#contact" href="#contact">Contact</a>
</div>
<div class="hc-urlbar" id="urlBar">location.hash = <strong id="hashVal">""</strong></div>
<div class="hc-page">
<div class="hc-page-bar"><div class="hc-page-dot"></div><div class="hc-page-dot"></div><div class="hc-page-dot"></div></div>
<div class="hc-page-body">
<span class="hc-page-content" id="pageContent">click a link above</span>
</div>
</div>
<div class="hc-console">
<span class="hc-cline" id="c1">from: ...</span>
<span class="hc-cline" id="c2">to: ...</span>
</div>
<p class="hc-note">No page reload โ this is how simple client-side routers used to work before the History API took over.</p>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-hashchange')
|| document.querySelector('.infographic-hashchange');
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');
const hashVal = wrapper.querySelector('#hashVal');
const pageContent = wrapper.querySelector('#pageContent');
const c1 = wrapper.querySelector('#c1');
const c2 = wrapper.querySelector('#c2');
const navlinks = wrapper.querySelectorAll('.hc-navlink');
const pages = {
'#home': 'Home page content',
'#about': 'About page content',
'#contact': 'Contact page content',
};
function render(hash) {
hashVal.textContent = '"' + hash + '"';
navlinks.forEach(l => l.classList.toggle('is-active', l.dataset.hash === hash));
pageContent.classList.remove('is-shown');
setTimeout(() => {
pageContent.textContent = pages[hash] || 'click a link above';
pageContent.classList.add('is-shown');
}, 120);
}
window.addEventListener('hashchange', (e) => {
c1.textContent = 'from: ...' + e.oldURL.split('#')[1] ? '#' + e.oldURL.split('#')[1] : e.oldURL;
c1.textContent = 'from: ' + e.oldURL.replace(/^.*\/\/[^/]*/, '');
c2.textContent = 'to: ' + e.newURL.replace(/^.*\/\/[^/]*/, '');
c1.classList.add('is-shown');
c2.classList.add('is-shown');
render(location.hash);
});
navlinks.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
location.hash = link.dataset.hash;
});
});
// initial state
if (location.hash && pages[location.hash]) {
render(location.hash);
}
})();
</script>๐ Example URL
example.com/#profile
example.com/#settings๐ง How It Works
Clicking anchor links changes hash
Page does not reload
hashchangeevent fires
๐ Real-World Uses
โ Single Page Applications (SPA)
โ Tab navigation
โ Section routing
๐ง Quick Summary
Event Type | Used For |
|---|---|
Mouse | Hover, clicks |
Keyboard | Shortcuts, forms |
Scroll | Animations, loading |
scrollIntoView | Smooth navigation |
Focus | Form UX |
hashchange | SPA routing |