π History API
The History API allows JavaScript to control the browser's navigation history.
It is essential for building Single Page Applications (SPAs) where URLs change without reloading the page.
π What Is the History API?
The browser maintains a history stack, like a list of visited pages.
[ Page1 ] β [ Page2 ] β [ Page3 ] β ...The History API lets you:
Move through this stack (
back(),forward(),go())Add or modify history entries (
pushState(),replaceState())React to user navigation (
popstateevent)
<div class="infographic-historystack">
<style>
.infographic-historystack {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--bad: #c23b3b;
--ghost: #b9c4d6;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
box-sizing: border-box;
max-width: 860px;
margin: 0 auto;
font-family: 'Inter', system-ui, sans-serif;
color: var(--ink);
background: var(--paper);
overflow-x: hidden;
}
.infographic-historystack[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--bad: #e06b6b;
--ghost: #3a4152;
}
.infographic-historystack * { box-sizing: border-box; }
.infographic-historystack h1 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-historystack p { margin: 0; }
.infographic-historystack button { font-family: inherit; }
.hs-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.hs-icon svg { width: 100%; height: 100%; display: block; }
.hs-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.hs-hero { text-align: center; margin-bottom: 1.3rem; }
.hs-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.hs-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
/* --- mini browser chrome --- */
.hs-chrome { max-width: 460px; margin: 0 auto 1.6rem; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.hs-chrome-bar { display: flex; align-items: center; gap: 0.5rem; padding: 0.5rem 0.7rem; background: var(--paper-alt); border-bottom: 1px solid var(--line); }
.hs-nav-btn { display: flex; align-items: center; justify-content: center; width: 26px; height: 26px; border-radius: 50%;
border: 1px solid var(--line); background: var(--paper); color: var(--ink-soft); cursor: pointer; flex-shrink: 0; }
.hs-nav-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.hs-nav-btn:disabled { opacity: 0.35; cursor: not-allowed; }
.hs-nav-btn .hs-icon { width: 13px; height: 13px; }
.hs-url { flex: 1; font-family: var(--mono-font); font-size: 0.72rem; background: var(--paper); border: 1px solid var(--line);
border-radius: 20px; padding: 0.35rem 0.8rem; color: var(--ink); text-align: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.hs-chrome-body { padding: 1.1rem 1rem 0.9rem; text-align: center; }
.hs-page-label { font-family: var(--display-font); font-size: 1.4rem; font-weight: 600; color: var(--accent); margin-bottom: 0.6rem; }
.hs-visit-btn { display: inline-flex; align-items: center; gap: 0.35rem; background: var(--accent); color: #fff; border: none;
border-radius: 6px; padding: 0.45rem 1rem; font-size: 0.76rem; font-weight: 700; cursor: pointer; }
.hs-visit-btn:hover { opacity: 0.88; }
.hs-visit-btn .hs-icon { width: 13px; height: 13px; }
/* --- stack visual --- */
.hs-stack-label { text-align: center; font-family: var(--mono-font); font-size: 0.66rem; color: var(--ink-soft);
text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 0.6rem; }
.hs-stack-wrap { position: relative; }
.hs-stack-row { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; gap: 0; min-height: 3.4rem; margin-bottom: 0.3rem; }
.hs-entry-group { display: flex; align-items: center; }
.hs-entry { display: flex; align-items: center; justify-content: center; width: 44px; height: 34px; border-radius: 6px;
font-family: var(--mono-font); font-size: 0.78rem; font-weight: 700; border: 2px solid var(--line); color: var(--ink-soft);
background: var(--paper); transition: all 0.2s ease; position: relative; }
.hs-entry.is-visited { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.hs-entry.is-current { border-color: var(--good); color: var(--good); background: var(--paper); box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 20%, transparent); }
.hs-entry.is-ghost { border-color: var(--ghost); color: var(--ghost); border-style: dashed; background: transparent; opacity: 0.7; }
.hs-arrow { color: var(--ink-soft); font-size: 0.9rem; margin: 0 0.15rem; opacity: 0.6; }
.hs-arrow.is-ghost { color: var(--ghost); opacity: 0.7; }
/* --- console --- */
.hs-console-panel { max-width: 480px; margin: 0 auto 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.hs-console-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--line); background: var(--paper-alt); font-family: var(--mono-font); font-size: 0.7rem;
font-weight: 700; color: #888; }
.hs-clear-btn { background: none; border: none; color: var(--ink-soft); font-family: var(--mono-font); font-size: 0.64rem;
cursor: pointer; padding: 0.15rem 0.4rem; border-radius: 4px; }
.hs-clear-btn:hover { color: var(--accent); background: var(--accent-soft); }
.hs-console { min-height: 5.5rem; max-height: 8rem; overflow-y: auto; background: #1e1e1e; padding: 0.5rem 0.7rem; }
.hs-console-line { font-family: var(--mono-font); font-size: 0.66rem; color: #d4d4d4; padding: 0.08rem 0; animation: hs-pop-in 0.2s ease; white-space: pre-wrap; }
.hs-console-line.is-echo { color: #7fb0ff; }
.hs-console-line.is-good { color: #9fd9a8; padding-left: 0.9rem; }
.hs-console-line.is-warn { color: #e0a95f; padding-left: 0.9rem; }
.hs-console-empty { font-family: var(--mono-font); font-size: 0.66rem; color: #6b6b6b; }
@keyframes hs-pop-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
.hs-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.4rem; max-width: 560px; margin: 0 auto; }
.hs-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-historystack *, .infographic-historystack *::before, .infographic-historystack *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="hs-wrap">
<div class="hs-chrome">
<div class="hs-chrome-bar">
<button class="hs-nav-btn" id="hsBackBtn" type="button" title="history.back()"><span class="hs-icon" id="hsBackIcon"></span></button>
<button class="hs-nav-btn" id="hsFwdBtn" type="button" title="history.forward()"><span class="hs-icon" id="hsFwdIcon"></span></button>
<div class="hs-url" id="hsUrl">example.com/page-a</div>
</div>
<div class="hs-chrome-body">
<div class="hs-page-label" id="hsPageLabel">Page A</div>
<button class="hs-visit-btn" id="hsVisitBtn" type="button">
<span class="hs-icon" id="hsVisitIcon"></span>Visit a new page
</button>
</div>
</div>
<div class="hs-stack-label">history stack</div>
<div class="hs-stack-wrap" id="hsStackWrap">
<div class="hs-stack-row" id="hsStackRow"></div>
</div>
<div class="hs-console-panel">
<div class="hs-console-head">
<span>>_ event log</span>
<button class="hs-clear-btn" id="hsClearBtn" type="button">clear</button>
</div>
<div class="hs-console" id="hsConsole">
<div class="hs-console-empty">// visit a page, then try back / forward</div>
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-historystack')
|| document.querySelector('.infographic-historystack');
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 ICONS = {
chevLeft: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>',
chevRight: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>',
plus: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>'
};
wrapper.querySelector('#hsBackIcon').innerHTML = ICONS.chevLeft;
wrapper.querySelector('#hsFwdIcon').innerHTML = ICONS.chevRight;
wrapper.querySelector('#hsVisitIcon').innerHTML = ICONS.plus;
const backBtn = wrapper.querySelector('#hsBackBtn');
const fwdBtn = wrapper.querySelector('#hsFwdBtn');
const visitBtn = wrapper.querySelector('#hsVisitBtn');
const urlEl = wrapper.querySelector('#hsUrl');
const pageLabel = wrapper.querySelector('#hsPageLabel');
const stackRow = wrapper.querySelector('#hsStackRow');
const consoleEl = wrapper.querySelector('#hsConsole');
const clearBtn = wrapper.querySelector('#hsClearBtn');
let stack = ['A'];
let pointer = 0;
let nextLetterCode = 66; // 'B'
function logLine(text, cls) {
if (consoleEl.querySelector('.hs-console-empty')) consoleEl.innerHTML = '';
const line = document.createElement('div');
line.className = 'hs-console-line ' + (cls || '');
line.textContent = text;
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
function render() {
pageLabel.textContent = 'Page ' + stack[pointer];
urlEl.textContent = 'example.com/page-' + stack[pointer].toLowerCase();
backBtn.disabled = pointer === 0;
fwdBtn.disabled = pointer === stack.length - 1;
stackRow.innerHTML = '';
stack.forEach((letter, i) => {
if (i > 0) {
const arrow = document.createElement('span');
arrow.className = 'hs-arrow';
arrow.textContent = '\u2192';
stackRow.appendChild(arrow);
}
const entry = document.createElement('div');
entry.className = 'hs-entry ' + (i === pointer ? 'is-current' : 'is-visited');
entry.textContent = letter;
stackRow.appendChild(entry);
});
}
backBtn.addEventListener('click', () => {
if (pointer === 0) return;
pointer -= 1;
logLine('> history.back();', 'is-echo');
logLine('pointer \u2192 Page ' + stack[pointer], 'is-good');
render();
});
fwdBtn.addEventListener('click', () => {
if (pointer === stack.length - 1) return;
pointer += 1;
logLine('> history.forward();', 'is-echo');
logLine('pointer \u2192 Page ' + stack[pointer], 'is-good');
render();
});
visitBtn.addEventListener('click', () => {
const newLetter = String.fromCharCode(nextLetterCode);
nextLetterCode += 1;
if (nextLetterCode > 90) nextLetterCode = 65;
const discarded = stack.length - 1 - pointer;
logLine("> history.pushState(null, '', '/page-" + newLetter.toLowerCase() + "');", 'is-echo');
if (discarded > 0) {
logLine('discarded ' + discarded + ' forward entr' + (discarded === 1 ? 'y' : 'ies') + ': [' + stack.slice(pointer + 1).join(', ') + ']', 'is-warn');
}
stack = stack.slice(0, pointer + 1);
stack.push(newLetter);
pointer = stack.length - 1;
logLine('pushed Page ' + newLetter + ' \u2014 now current', 'is-good');
render();
});
clearBtn.addEventListener('click', () => {
consoleEl.innerHTML = '<div class="hs-console-empty">// visit a page, then try back / forward</div>';
});
render();
})();
</script>π Basic History API Methods
π 1 history.length
Shows how many entries exist in the session history.
console.log(history.length);π 2 history.back()
Same as pressing browser Back button.
history.back();π 3 history.forward()
Moves forward in the stack if possible.
history.forward();π 4 history.go(n)
history.go(-2); // Go 2 pages back
history.go(1); // Go forward<div class="infographic-historymethods">
<style>
.infographic-historymethods {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--bad: #c23b3b;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
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-historymethods[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
--bad: #e06b6b;
}
.infographic-historymethods * { box-sizing: border-box; }
.infographic-historymethods h1 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-historymethods p { margin: 0; }
.infographic-historymethods button, .infographic-historymethods input { font-family: inherit; }
.hm-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.hm-icon svg { width: 100%; height: 100%; display: block; }
.hm-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.hm-hero { text-align: center; margin-bottom: 1.4rem; }
.hm-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.hm-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
/* --- stack visual --- */
.hm-stack-label { text-align: center; font-family: var(--mono-font); font-size: 0.66rem; color: var(--ink-soft);
text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 0.6rem; }
.hm-stack-wrap { position: relative; }
.hm-stack-row { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; min-height: 3.4rem; margin-bottom: 0.3rem; }
.hm-entry { display: flex; align-items: center; justify-content: center; width: 44px; height: 34px; border-radius: 6px;
font-family: var(--mono-font); font-size: 0.78rem; font-weight: 700; border: 2px solid var(--line); color: var(--ink-soft);
background: var(--paper); transition: all 0.2s ease; flex-shrink: 0; }
.hm-entry.is-visited { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.hm-entry.is-current { border-color: var(--good); color: var(--good); background: var(--paper); box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 20%, transparent); }
.hm-arrow { color: var(--ink-soft); font-size: 0.9rem; margin: 0 0.15rem; opacity: 0.6; flex-shrink: 0; }
.hm-length-readout { text-align: center; font-family: var(--mono-font); font-size: 0.72rem; color: var(--ink-soft); margin-bottom: 1.5rem; }
.hm-length-readout strong { color: var(--accent); }
/* --- methods --- */
.hm-methods { display: flex; flex-direction: column; gap: 0.55rem; max-width: 560px; margin: 0 auto 1.3rem; }
.hm-method { display: flex; align-items: center; gap: 0.65rem; border: 1px solid var(--line); border-radius: 7px; padding: 0.5rem 0.65rem; }
.hm-method-icon { width: 26px; height: 26px; border-radius: 6px; background: var(--accent-soft); color: var(--accent);
display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
.hm-method-icon .hm-icon { width: 14px; height: 14px; }
.hm-method-body { flex: 1; min-width: 0; }
.hm-method-title { font-family: var(--mono-font); font-size: 0.74rem; font-weight: 700; color: var(--ink); }
.hm-method-desc { font-size: 0.68rem; color: var(--ink-soft); }
.hm-go-input { width: 3.2rem; border: 1px solid var(--line); border-radius: 5px; padding: 0.3rem 0.4rem; font-family: var(--mono-font);
font-size: 0.72rem; text-align: center; color: var(--ink); background: var(--paper); flex-shrink: 0; }
.hm-run-btn { flex-shrink: 0; background: var(--accent); color: #fff; border: none; border-radius: 5px; padding: 0.35rem 0.75rem;
font-size: 0.68rem; font-weight: 700; cursor: pointer; font-family: var(--mono-font); }
.hm-run-btn:hover { opacity: 0.88; }
.hm-run-btn:disabled { background: var(--line); color: var(--ink-soft); cursor: not-allowed; }
/* --- console --- */
.hm-console-panel { max-width: 520px; margin: 0 auto 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.hm-console-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--line); background: var(--paper-alt); font-family: var(--mono-font); font-size: 0.7rem;
font-weight: 700; color: #888; }
.hm-clear-btn { background: none; border: none; color: var(--ink-soft); font-family: var(--mono-font); font-size: 0.64rem;
cursor: pointer; padding: 0.15rem 0.4rem; border-radius: 4px; }
.hm-clear-btn:hover { color: var(--accent); background: var(--accent-soft); }
.hm-console { min-height: 6rem; max-height: 8.5rem; overflow-y: auto; background: #1e1e1e; padding: 0.5rem 0.7rem; }
.hm-console-line { font-family: var(--mono-font); font-size: 0.68rem; color: #d4d4d4; padding: 0.08rem 0; animation: hm-pop-in 0.2s ease; white-space: pre-wrap; }
.hm-console-line.is-echo { color: #7fb0ff; }
.hm-console-line.is-good { color: #9fd9a8; padding-left: 0.9rem; }
.hm-console-line.is-warn { color: #e0a95f; padding-left: 0.9rem; }
.hm-console-empty { font-family: var(--mono-font); font-size: 0.68rem; color: #6b6b6b; }
@keyframes hm-pop-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
.hm-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.4rem; max-width: 560px; margin: 0 auto; }
.hm-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-historymethods *, .infographic-historymethods *::before, .infographic-historymethods *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="hm-wrap">
<div class="hm-stack-label">history stack</div>
<div class="hm-stack-wrap" id="hmStackWrap">
<div class="hm-stack-row" id="hmStackRow"></div>
</div>
<div class="hm-length-readout">history.length = <strong id="hmLengthVal">4</strong></div>
<div class="hm-methods" id="hmMethods">
<div class="hm-method" data-method="length">
<span class="hm-method-icon"><span class="hm-icon" id="hmIcon0"></span></span>
<span class="hm-method-body">
<div class="hm-method-title">history.length</div>
<div class="hm-method-desc">how many entries exist β read only</div>
</span>
<button class="hm-run-btn" data-run="length" type="button">Run</button>
</div>
<div class="hm-method" data-method="back">
<span class="hm-method-icon"><span class="hm-icon" id="hmIcon1"></span></span>
<span class="hm-method-body">
<div class="hm-method-title">history.back()</div>
<div class="hm-method-desc">same as pressing the Back button</div>
</span>
<button class="hm-run-btn" data-run="back" type="button">Run</button>
</div>
<div class="hm-method" data-method="forward">
<span class="hm-method-icon"><span class="hm-icon" id="hmIcon2"></span></span>
<span class="hm-method-body">
<div class="hm-method-title">history.forward()</div>
<div class="hm-method-desc">moves forward, if there's somewhere to go</div>
</span>
<button class="hm-run-btn" data-run="forward" type="button">Run</button>
</div>
<div class="hm-method" data-method="go">
<span class="hm-method-icon"><span class="hm-icon" id="hmIcon3"></span></span>
<span class="hm-method-body">
<div class="hm-method-title">history.go(n)</div>
<div class="hm-method-desc">jump n steps β negative goes back</div>
</span>
<input class="hm-go-input" id="hmGoInput" type="number" value="-2" step="1">
<button class="hm-run-btn" data-run="go" type="button">Run</button>
</div>
</div>
<div class="hm-console-panel">
<div class="hm-console-head">
<span>>_ event log</span>
<button class="hm-clear-btn" id="hmClearBtn" type="button">clear</button>
</div>
<div class="hm-console" id="hmConsole">
<div class="hm-console-empty">// stack starts at [A] β [B] β [C] β [D], current: D</div>
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-historymethods')
|| document.querySelector('.infographic-historymethods');
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 ICONS = {
hash: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="4" x2="20" y1="9" y2="9"/><line x1="4" x2="20" y1="15" y2="15"/><line x1="10" x2="8" y1="3" y2="21"/><line x1="16" x2="14" y1="3" y2="21"/></svg>',
chevLeft: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>',
chevRight: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>',
target: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="12" r="6"/><circle cx="12" cy="12" r="2"/></svg>'
};
wrapper.querySelector('#hmIcon0').innerHTML = ICONS.hash;
wrapper.querySelector('#hmIcon1').innerHTML = ICONS.chevLeft;
wrapper.querySelector('#hmIcon2').innerHTML = ICONS.chevRight;
wrapper.querySelector('#hmIcon3').innerHTML = ICONS.target;
const stackRow = wrapper.querySelector('#hmStackRow');
const lengthVal = wrapper.querySelector('#hmLengthVal');
const goInput = wrapper.querySelector('#hmGoInput');
const consoleEl = wrapper.querySelector('#hmConsole');
const clearBtn = wrapper.querySelector('#hmClearBtn');
const runBtns = wrapper.querySelectorAll('[data-run]');
const stack = ['A', 'B', 'C', 'D'];
let pointer = 3;
function logLine(text, cls) {
if (consoleEl.querySelector('.hm-console-empty')) consoleEl.innerHTML = '';
const line = document.createElement('div');
line.className = 'hm-console-line ' + (cls || '');
line.textContent = text;
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
function render() {
lengthVal.textContent = String(stack.length);
stackRow.innerHTML = '';
stack.forEach((letter, i) => {
if (i > 0) {
const arrow = document.createElement('span');
arrow.className = 'hm-arrow';
arrow.textContent = '\u2192';
stackRow.appendChild(arrow);
}
const entry = document.createElement('div');
entry.className = 'hm-entry ' + (i === pointer ? 'is-current' : 'is-visited');
entry.textContent = letter;
stackRow.appendChild(entry);
});
}
function goTo(delta, label) {
const target = pointer + delta;
if (target < 0 || target > stack.length - 1) {
logLine(label, 'is-echo');
logLine('no-op \u2014 out of range, pointer stays at ' + stack[pointer], 'is-warn');
return;
}
pointer = target;
logLine(label, 'is-echo');
logLine('current \u2192 Page ' + stack[pointer], 'is-good');
render();
}
runBtns.forEach(btn => {
btn.addEventListener('click', () => {
const method = btn.dataset.run;
if (method === 'length') {
logLine('> console.log(history.length);', 'is-echo');
logLine(String(stack.length), 'is-good');
} else if (method === 'back') {
goTo(-1, '> history.back();');
} else if (method === 'forward') {
goTo(1, '> history.forward();');
} else if (method === 'go') {
const n = parseInt(goInput.value, 10) || 0;
goTo(n, '> history.go(' + n + ');');
}
});
});
clearBtn.addEventListener('click', () => {
consoleEl.innerHTML = '<div class="hm-console-empty">// stack starts at [A] β [B] β [C] β [D], current: D</div>';
});
render();
})();
</script>π§ Manipulating Browser History Without Page Reload
Normally, when you change a URL:
/home β /profileπ The browser reloads the page
π HTML, CSS, JS are downloaded again
π State is lost
π Traditional Navigation (Reload Happens)
<a href="/profile">Profile</a>Click link
β Browser requests /profile
β Page reload
β JS starts againπ What History API Changes
The History API allows you to:
Change the URL AND browser history WITHOUT reloading the page
This is the foundation of SPAs (React, Angular, Vue, Next.js).
π Core Idea (Very Important)
URL β Page ReloadWith History API:
URL can change
History stack can update
UI updates manually using JS
Browser back/forward still works
π The Two Main Methods
1οΈβ£ history.pushState()
2οΈβ£ history.replaceState()
π 1. history.pushState() β Add New History Entry
history.pushState(stateObject, title, url);Parameters Explained
Parameter | Meaning |
|---|---|
| JS data saved with this history entry |
| Ignored by most browsers |
| New URL shown in address bar |
<div class="infographic-pushstate">
<style>
.infographic-pushstate {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
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-pushstate[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
}
.infographic-pushstate * { box-sizing: border-box; }
.infographic-pushstate h1 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-pushstate p { margin: 0; }
.infographic-pushstate button { font-family: inherit; }
.ps-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.ps-icon svg { width: 100%; height: 100%; display: block; }
.ps-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.ps-hero { text-align: center; margin-bottom: 1.2rem; }
.ps-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.ps-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
.ps-code { background: #1e1e1e; color: #d4d4d4; border-radius: 6px; padding: 0.7rem 0.9rem; font-family: var(--mono-font);
font-size: 0.72rem; margin: 0 auto 1.4rem; max-width: 320px; text-align: left; white-space: pre; }
.tok-fn { color: #dcdcaa; }
.tok-punc { color: #d4d4d4; }
.tok-var { color: #9cdcfe; }
.tok-prop { color: #9cdcfe; }
.tok-str { color: #ce9178; }
/* --- stack visual --- */
.ps-stack-label { text-align: center; font-family: var(--mono-font); font-size: 0.66rem; color: var(--ink-soft);
text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 0.6rem; }
.ps-stack-row { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; min-height: 3.4rem; margin-bottom: 1rem; }
.ps-entry { display: flex; align-items: center; justify-content: center; padding: 0 0.7rem; height: 34px; border-radius: 6px;
font-family: var(--mono-font); font-size: 0.78rem; font-weight: 700; border: 2px solid var(--line); color: var(--ink-soft);
background: var(--paper); transition: all 0.2s ease; }
.ps-entry.is-visited { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.ps-entry.is-current { border-color: var(--good); color: var(--good); background: var(--paper); box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 20%, transparent); }
.ps-arrow { color: var(--ink-soft); font-size: 0.9rem; margin: 0 0.15rem; opacity: 0.6; }
/* --- state readout --- */
.ps-state-readout { max-width: 380px; margin: 0 auto 1.3rem; border: 1px solid var(--line); border-radius: 6px;
padding: 0.55rem 0.8rem; font-family: var(--mono-font); font-size: 0.7rem; text-align: center; color: var(--ink-soft); }
.ps-state-readout strong { color: var(--accent); }
/* --- controls --- */
.ps-controls { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; margin-bottom: 1.3rem; }
.ps-btn { display: flex; align-items: center; gap: 0.35rem; background: var(--accent); color: #fff; border: none;
border-radius: 6px; padding: 0.45rem 0.95rem; font-size: 0.76rem; font-weight: 700; cursor: pointer; }
.ps-btn:hover { opacity: 0.88; }
.ps-btn:disabled { background: var(--line); color: var(--ink-soft); cursor: not-allowed; }
.ps-btn.is-ghost { background: var(--paper); color: var(--ink-soft); border: 1px solid var(--line); }
.ps-btn.is-ghost:hover { border-color: var(--accent); color: var(--accent); }
.ps-btn .ps-icon { width: 13px; height: 13px; }
/* --- console --- */
.ps-console-panel { max-width: 480px; margin: 0 auto 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.ps-console-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--line); background: var(--paper-alt); font-family: var(--mono-font); font-size: 0.7rem;
font-weight: 700; color: #888; }
.ps-clear-btn { background: none; border: none; color: var(--ink-soft); font-family: var(--mono-font); font-size: 0.64rem;
cursor: pointer; padding: 0.15rem 0.4rem; border-radius: 4px; }
.ps-clear-btn:hover { color: var(--accent); background: var(--accent-soft); }
.ps-console { min-height: 6rem; max-height: 8.5rem; overflow-y: auto; background: #1e1e1e; padding: 0.5rem 0.7rem; }
.ps-console-line { font-family: var(--mono-font); font-size: 0.66rem; color: #d4d4d4; padding: 0.08rem 0; animation: ps-pop-in 0.2s ease; white-space: pre-wrap; }
.ps-console-line.is-echo { color: #7fb0ff; }
.ps-console-line.is-good { color: #9fd9a8; padding-left: 0.9rem; }
.ps-console-empty { font-family: var(--mono-font); font-size: 0.66rem; color: #6b6b6b; }
@keyframes ps-pop-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
.ps-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.4rem; max-width: 560px; margin: 0 auto; }
.ps-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-pushstate *, .infographic-pushstate *::before, .infographic-pushstate *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="ps-wrap">
<div class="ps-code"><span class="tok-var">history</span><span class="tok-punc">.</span><span class="tok-fn">pushState</span><span class="tok-punc">(</span>
<span class="tok-punc">{</span> <span class="tok-prop">page</span><span class="tok-punc">:</span> <span class="tok-str">"profile"</span> <span class="tok-punc">},</span>
<span class="tok-str">""</span><span class="tok-punc">,</span>
<span class="tok-str">"/profile"</span>
<span class="tok-punc">);</span></div>
<div class="ps-stack-label">history stack</div>
<div class="ps-stack-row" id="psStackRow"></div>
<div class="ps-state-readout">history.state = <strong id="psStateVal">null</strong></div>
<div class="ps-controls">
<button class="ps-btn" id="psPushBtn" type="button">
<span class="ps-icon" id="psPushIcon"></span><span id="psPushLabel">Run pushState({page: "profile"}, "", "/profile")</span>
</button>
<button class="ps-btn is-ghost" id="psResetBtn" type="button">
<span class="ps-icon" id="psResetIcon"></span>Reset
</button>
</div>
<div class="ps-console-panel">
<div class="ps-console-head">
<span>>_ event log</span>
<button class="ps-clear-btn" id="psClearBtn" type="button">clear</button>
</div>
<div class="ps-console" id="psConsole">
<div class="ps-console-empty">// click "Run pushState" to begin</div>
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-pushstate')
|| document.querySelector('.infographic-pushstate');
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 ICONS = {
plus: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><path d="M8 12h8"/><path d="M12 8v8"/></svg>',
refresh: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 3v6h-6"/></svg>'
};
wrapper.querySelector('#psPushIcon').innerHTML = ICONS.plus;
wrapper.querySelector('#psResetIcon').innerHTML = ICONS.refresh;
const stackRow = wrapper.querySelector('#psStackRow');
const stateVal = wrapper.querySelector('#psStateVal');
const pushBtn = wrapper.querySelector('#psPushBtn');
const pushLabel = wrapper.querySelector('#psPushLabel');
const resetBtn = wrapper.querySelector('#psResetBtn');
const consoleEl = wrapper.querySelector('#psConsole');
const clearBtn = wrapper.querySelector('#psClearBtn');
const PUSHES = [
{ label: 'Home', page: null, url: '/', state: null },
{ label: 'Profile', page: 'profile', url: '/profile', state: '{page: "profile"}' },
{ label: 'Settings', page: 'settings', url: '/settings', state: '{page: "settings"}' },
{ label: 'Dashboard', page: 'dashboard', url: '/dashboard', state: '{page: "dashboard"}' }
];
let stack = [PUSHES[0]];
function logLine(text, cls) {
if (consoleEl.querySelector('.ps-console-empty')) consoleEl.innerHTML = '';
const line = document.createElement('div');
line.className = 'ps-console-line ' + (cls || '');
line.textContent = text;
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
function render() {
stackRow.innerHTML = '';
stack.forEach((entry, i) => {
if (i > 0) {
const arrow = document.createElement('span');
arrow.className = 'ps-arrow';
arrow.textContent = '\u2192';
stackRow.appendChild(arrow);
}
const el = document.createElement('div');
el.className = 'ps-entry ' + (i === stack.length - 1 ? 'is-current' : 'is-visited');
el.textContent = entry.label;
stackRow.appendChild(el);
});
const current = stack[stack.length - 1];
stateVal.textContent = current.state || 'null';
if (stack.length < PUSHES.length) {
const next = PUSHES[stack.length];
pushLabel.textContent = 'Run pushState({page: "' + next.page + '"}, "", "' + next.url + '")';
pushBtn.disabled = false;
} else {
pushLabel.textContent = 'All example pages pushed';
pushBtn.disabled = true;
}
}
pushBtn.addEventListener('click', () => {
if (stack.length >= PUSHES.length) return;
const next = PUSHES[stack.length];
logLine('> history.pushState({page: "' + next.page + '"}, "", "' + next.url + '");', 'is-echo');
stack.push(next);
logLine('history.state \u25b8 ' + next.state, 'is-good');
logLine('URL bar \u25b8 ' + next.url + ' (no reload, no request sent)', 'is-good');
render();
});
resetBtn.addEventListener('click', () => {
stack = [PUSHES[0]];
render();
consoleEl.innerHTML = '<div class="ps-console-empty">// click "Run pushState" to begin</div>';
});
render();
})();
</script>β URL changes to /profile
β Page does NOT reload
β New history entry is added
You MUST Update UI Manually
history.pushState({ page: "profile" }, "", "/profile");
renderProfilePage();History API:
Changes URL
Changes history
DOES NOT change UI automatically
π 2. history.replaceState() β Replace Current Entry
<div class="infographic-replacestate">
<style>
.infographic-replacestate {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
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-replacestate[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
}
.infographic-replacestate * { box-sizing: border-box; }
.infographic-replacestate h1 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-replacestate p { margin: 0; }
.infographic-replacestate button { font-family: inherit; }
.rs-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.rs-icon svg { width: 100%; height: 100%; display: block; }
.rs-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.rs-hero { text-align: center; margin-bottom: 1.2rem; }
.rs-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.rs-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
.rs-code { background: #1e1e1e; color: #d4d4d4; border-radius: 6px; padding: 0.7rem 0.9rem; font-family: var(--mono-font);
font-size: 0.72rem; margin: 0 auto 1.4rem; max-width: 320px; text-align: left; white-space: pre; }
.tok-fn { color: #dcdcaa; }
.tok-punc { color: #d4d4d4; }
.tok-var { color: #9cdcfe; }
.tok-prop { color: #9cdcfe; }
.tok-str { color: #ce9178; }
/* --- stack visual --- */
.rs-stack-label { text-align: center; font-family: var(--mono-font); font-size: 0.66rem; color: var(--ink-soft);
text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 0.6rem; }
.rs-stack-row { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; min-height: 3.4rem; margin-bottom: 1rem; }
.rs-entry { display: flex; align-items: center; justify-content: center; padding: 0 0.7rem; height: 34px; border-radius: 6px;
font-family: var(--mono-font); font-size: 0.78rem; font-weight: 700; border: 2px solid var(--line); color: var(--ink-soft);
background: var(--paper); transition: all 0.2s ease; }
.rs-entry.is-visited { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.rs-entry.is-current { border-color: var(--good); color: var(--good); background: var(--paper); box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 20%, transparent); }
.rs-entry.is-swapping { animation: rs-swap 0.4s ease; }
@keyframes rs-swap { 0% { transform: scale(1); } 40% { transform: scale(0.85); opacity: 0.4; } 100% { transform: scale(1); opacity: 1; } }
.rs-arrow { color: var(--ink-soft); font-size: 0.9rem; margin: 0 0.15rem; opacity: 0.6; }
/* --- readouts --- */
.rs-readouts { display: flex; gap: 0.6rem; justify-content: center; flex-wrap: wrap; max-width: 480px; margin: 0 auto 1.3rem; }
.rs-readout { border: 1px solid var(--line); border-radius: 6px; padding: 0.5rem 0.8rem; font-family: var(--mono-font);
font-size: 0.68rem; text-align: center; color: var(--ink-soft); }
.rs-readout strong { color: var(--accent); }
/* --- controls --- */
.rs-controls { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; margin-bottom: 1.3rem; }
.rs-btn { display: flex; align-items: center; gap: 0.35rem; background: var(--accent); color: #fff; border: none;
border-radius: 6px; padding: 0.45rem 0.95rem; font-size: 0.74rem; font-weight: 700; cursor: pointer; }
.rs-btn:hover { opacity: 0.88; }
.rs-btn:disabled { background: var(--line); color: var(--ink-soft); cursor: not-allowed; }
.rs-btn.is-ghost { background: var(--paper); color: var(--ink-soft); border: 1px solid var(--line); }
.rs-btn.is-ghost:hover { border-color: var(--accent); color: var(--accent); }
.rs-btn .rs-icon { width: 13px; height: 13px; }
/* --- console --- */
.rs-console-panel { max-width: 480px; margin: 0 auto 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.rs-console-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--line); background: var(--paper-alt); font-family: var(--mono-font); font-size: 0.7rem;
font-weight: 700; color: #888; }
.rs-clear-btn { background: none; border: none; color: var(--ink-soft); font-family: var(--mono-font); font-size: 0.64rem;
cursor: pointer; padding: 0.15rem 0.4rem; border-radius: 4px; }
.rs-clear-btn:hover { color: var(--accent); background: var(--accent-soft); }
.rs-console { min-height: 6rem; max-height: 8.5rem; overflow-y: auto; background: #1e1e1e; padding: 0.5rem 0.7rem; }
.rs-console-line { font-family: var(--mono-font); font-size: 0.66rem; color: #d4d4d4; padding: 0.08rem 0; animation: rs-pop-in 0.2s ease; white-space: pre-wrap; }
.rs-console-line.is-echo { color: #7fb0ff; }
.rs-console-line.is-good { color: #9fd9a8; padding-left: 0.9rem; }
.rs-console-empty { font-family: var(--mono-font); font-size: 0.66rem; color: #6b6b6b; }
@keyframes rs-pop-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
.rs-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.4rem; max-width: 560px; margin: 0 auto; }
.rs-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-replacestate *, .infographic-replacestate *::before, .infographic-replacestate *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="rs-wrap">
<div class="rs-code"><span class="tok-var">history</span><span class="tok-punc">.</span><span class="tok-fn">replaceState</span><span class="tok-punc">(</span>
<span class="tok-punc">{</span> <span class="tok-prop">page</span><span class="tok-punc">:</span> <span class="tok-str">"dashboard"</span> <span class="tok-punc">},</span>
<span class="tok-str">""</span><span class="tok-punc">,</span>
<span class="tok-str">"/dashboard"</span>
<span class="tok-punc">);</span></div>
<div class="rs-stack-label">history stack</div>
<div class="rs-stack-row" id="rsStackRow"></div>
<div class="rs-readouts">
<div class="rs-readout">history.length = <strong id="rsLengthVal">2</strong></div>
<div class="rs-readout">history.state = <strong id="rsStateVal">null</strong></div>
</div>
<div class="rs-controls">
<button class="rs-btn" id="rsReplaceBtn" type="button">
<span class="rs-icon" id="rsReplaceIcon"></span><span id="rsReplaceLabel">Run replaceState({page: "dashboard"}, "", "/dashboard")</span>
</button>
<button class="rs-btn is-ghost" id="rsResetBtn" type="button">
<span class="rs-icon" id="rsResetIcon"></span>Reset
</button>
</div>
<div class="rs-console-panel">
<div class="rs-console-head">
<span>>_ event log</span>
<button class="rs-clear-btn" id="rsClearBtn" type="button">clear</button>
</div>
<div class="rs-console" id="rsConsole">
<div class="rs-console-empty">// click "Run replaceState" to begin</div>
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-replacestate')
|| document.querySelector('.infographic-replacestate');
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 ICONS = {
swap: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m17 2 4 4-4 4"/><path d="M3 11v-1a4 4 0 0 1 4-4h14"/><path d="m7 22-4-4 4-4"/><path d="M21 13v1a4 4 0 0 1-4 4H3"/></svg>',
refresh: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 3v6h-6"/></svg>'
};
wrapper.querySelector('#rsReplaceIcon').innerHTML = ICONS.swap;
wrapper.querySelector('#rsResetIcon').innerHTML = ICONS.refresh;
const stackRow = wrapper.querySelector('#rsStackRow');
const lengthVal = wrapper.querySelector('#rsLengthVal');
const stateVal = wrapper.querySelector('#rsStateVal');
const replaceBtn = wrapper.querySelector('#rsReplaceBtn');
const replaceLabel = wrapper.querySelector('#rsReplaceLabel');
const resetBtn = wrapper.querySelector('#rsResetBtn');
const consoleEl = wrapper.querySelector('#rsConsole');
const clearBtn = wrapper.querySelector('#rsClearBtn');
const REPLACEMENTS = [
{ label: 'Dashboard', page: 'dashboard', url: '/dashboard', state: '{page: "dashboard"}' },
{ label: 'Settings', page: 'settings', url: '/settings', state: '{page: "settings"}' },
{ label: 'Profile', page: 'profile', url: '/profile', state: '{page: "profile"}' }
];
let stack = [
{ label: 'Home', state: null },
{ label: 'Login', state: null }
];
let replaceIndex = 0;
function logLine(text, cls) {
if (consoleEl.querySelector('.rs-console-empty')) consoleEl.innerHTML = '';
const line = document.createElement('div');
line.className = 'rs-console-line ' + (cls || '');
line.textContent = text;
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
function render(swappedEl) {
lengthVal.textContent = String(stack.length);
const current = stack[stack.length - 1];
stateVal.textContent = current.state || 'null';
stackRow.innerHTML = '';
stack.forEach((entry, i) => {
if (i > 0) {
const arrow = document.createElement('span');
arrow.className = 'rs-arrow';
arrow.textContent = '\u2192';
stackRow.appendChild(arrow);
}
const el = document.createElement('div');
el.className = 'rs-entry ' + (i === stack.length - 1 ? 'is-current' : 'is-visited');
if (i === stack.length - 1 && swappedEl) el.classList.add('is-swapping');
el.textContent = entry.label;
stackRow.appendChild(el);
});
if (replaceIndex < REPLACEMENTS.length) {
const next = REPLACEMENTS[replaceIndex];
replaceLabel.textContent = 'Run replaceState({page: "' + next.page + '"}, "", "' + next.url + '")';
replaceBtn.disabled = false;
} else {
replaceLabel.textContent = 'All example replacements used';
replaceBtn.disabled = true;
}
}
replaceBtn.addEventListener('click', () => {
if (replaceIndex >= REPLACEMENTS.length) return;
const next = REPLACEMENTS[replaceIndex];
const oldLabel = stack[stack.length - 1].label;
logLine('> history.replaceState({page: "' + next.page + '"}, "", "' + next.url + '");', 'is-echo');
stack[stack.length - 1] = { label: next.label, state: next.state };
replaceIndex += 1;
logLine(oldLabel + ' \u2192 ' + next.label + ' (same slot, length unchanged)', 'is-good');
logLine('history.state \u25b8 ' + next.state, 'is-good');
render(true);
});
resetBtn.addEventListener('click', () => {
stack = [
{ label: 'Home', state: null },
{ label: 'Login', state: null }
];
replaceIndex = 0;
render();
consoleEl.innerHTML = '<div class="rs-console-empty">// click "Run replaceState" to begin</div>';
});
clearBtn.addEventListener('click', () => {
consoleEl.innerHTML = '<div class="rs-console-empty">// click "Run replaceState" to begin</div>';
});
render();
})();
</script>β URL changes
β No new history entry
β Back button skips Login
When to Use replaceState()
Use Case | Why |
|---|---|
Login redirect | Prevent going back to login |
Fix wrong URL | Update without history pollution |
Step-based forms | Replace instead of pushing |
π popstate Event β Reacting to History Navigation
Triggered when user clicks Back or Forward, NOT when calling pushState/replaceState.
When popstate Fires
Action | popstate |
|---|---|
Browser Back button | β |
Browser Forward button | β |
history.go(-1) | β |
history.go(1) | β |
history.pushState() | β |
history.replaceState() | β |
<div class="infographic-popstate">
<style>
.infographic-popstate {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
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-popstate[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
}
.infographic-popstate * { box-sizing: border-box; }
.infographic-popstate h1 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-popstate p { margin: 0; }
.infographic-popstate button { font-family: inherit; }
.pop-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.pop-icon svg { width: 100%; height: 100%; display: block; }
.pop-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.pop-hero { text-align: center; margin-bottom: 1.2rem; }
.pop-hero h1 { font-size: clamp(1.35rem, 4.5vw, 1.85rem); }
.pop-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
.pop-code { background: #1e1e1e; color: #d4d4d4; border-radius: 6px; padding: 0.7rem 0.9rem; font-family: var(--mono-font);
font-size: 0.72rem; margin: 0 auto 1.4rem; max-width: 380px; text-align: left; white-space: pre; }
.tok-kw { color: #569cd6; }
.tok-fn { color: #dcdcaa; }
.tok-punc { color: #d4d4d4; }
.tok-var { color: #9cdcfe; }
.tok-str { color: #ce9178; }
/* --- stack visual --- */
.pop-stack-label { text-align: center; font-family: var(--mono-font); font-size: 0.66rem; color: var(--ink-soft);
text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 0.6rem; }
.pop-stack-row { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; min-height: 3.4rem; margin-bottom: 1.3rem; }
.pop-entry { display: flex; align-items: center; justify-content: center; padding: 0 0.7rem; height: 34px; border-radius: 6px;
font-family: var(--mono-font); font-size: 0.78rem; font-weight: 700; border: 2px solid var(--line); color: var(--ink-soft);
background: var(--paper); transition: all 0.2s ease; }
.pop-entry.is-visited { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.pop-entry.is-current { border-color: var(--good); color: var(--good); background: var(--paper); box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 20%, transparent); }
.pop-arrow { color: var(--ink-soft); font-size: 0.9rem; margin: 0 0.15rem; opacity: 0.6; }
/* --- controls --- */
.pop-controls { display: flex; gap: 0.5rem; justify-content: center; flex-wrap: wrap; margin-bottom: 1.3rem; }
.pop-btn { display: flex; align-items: center; gap: 0.35rem; background: var(--accent); color: #fff; border: none;
border-radius: 6px; padding: 0.45rem 0.95rem; font-size: 0.78rem; font-weight: 700; cursor: pointer; }
.pop-btn:hover:not(:disabled) { opacity: 0.88; }
.pop-btn:disabled { background: var(--line); color: var(--ink-soft); cursor: not-allowed; }
.pop-btn.is-ghost { background: var(--paper); color: var(--ink-soft); border: 1px solid var(--line); }
.pop-btn.is-ghost:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.pop-btn .pop-icon { width: 13px; height: 13px; }
/* --- console --- */
.pop-console-panel { max-width: 480px; margin: 0 auto 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.pop-console-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--line); background: var(--paper-alt); font-family: var(--mono-font); font-size: 0.7rem;
font-weight: 700; color: #888; }
.pop-clear-btn { background: none; border: none; color: var(--ink-soft); font-family: var(--mono-font); font-size: 0.64rem;
cursor: pointer; padding: 0.15rem 0.4rem; border-radius: 4px; }
.pop-clear-btn:hover { color: var(--accent); background: var(--accent-soft); }
.pop-console { min-height: 6rem; max-height: 8.5rem; overflow-y: auto; background: #1e1e1e; padding: 0.5rem 0.7rem; }
.pop-console-line { font-family: var(--mono-font); font-size: 0.66rem; color: #d4d4d4; padding: 0.08rem 0; animation: pop-pop-in 0.2s ease; white-space: pre-wrap; }
.pop-console-line.is-echo { color: #7fb0ff; }
.pop-console-line.is-event { color: #d8a657; padding-left: 0.9rem; }
.pop-console-line.is-good { color: #9fd9a8; padding-left: 0.9rem; }
.pop-console-empty { font-family: var(--mono-font); font-size: 0.66rem; color: #6b6b6b; }
@keyframes pop-pop-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
.pop-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.4rem; max-width: 560px; margin: 0 auto; }
.pop-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-popstate *, .infographic-popstate *::before, .infographic-popstate *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="pop-wrap">
<div class="pop-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">"popstate"</span><span class="tok-punc">,</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">state</span><span class="tok-punc">);</span>
<span class="tok-punc">});</span></div>
<div class="pop-stack-label">history stack</div>
<div class="pop-stack-row" id="popStackRow"></div>
<div class="pop-controls">
<button class="pop-btn" id="popBackBtn" type="button">
<span class="pop-icon" id="popBackIcon"></span>Press Back button
</button>
<button class="pop-btn is-ghost" id="popFwdBtn" type="button">
<span class="pop-icon" id="popFwdIcon"></span>Press Forward
</button>
<button class="pop-btn is-ghost" id="popResetBtn" type="button">
<span class="pop-icon" id="popResetIcon"></span>Reset
</button>
</div>
<div class="pop-console-panel">
<div class="pop-console-head">
<span>>_ event log</span>
<button class="pop-clear-btn" id="popClearBtn" type="button">clear</button>
</div>
<div class="pop-console" id="popConsole">
<div class="pop-console-empty">// this is a real popstate listener β press Back to fire it</div>
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-popstate')
|| document.querySelector('.infographic-popstate');
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 ICONS = {
chevLeft: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>',
chevRight: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>',
refresh: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-3-6.7"/><path d="M21 3v6h-6"/></svg>'
};
wrapper.querySelector('#popBackIcon').innerHTML = ICONS.chevLeft;
wrapper.querySelector('#popFwdIcon').innerHTML = ICONS.chevRight;
wrapper.querySelector('#popResetIcon').innerHTML = ICONS.refresh;
const stackRow = wrapper.querySelector('#popStackRow');
const backBtn = wrapper.querySelector('#popBackBtn');
const fwdBtn = wrapper.querySelector('#popFwdBtn');
const resetBtn = wrapper.querySelector('#popResetBtn');
const consoleEl = wrapper.querySelector('#popConsole');
const clearBtn = wrapper.querySelector('#popClearBtn');
const STACK = [
{ label: 'Home', state: { page: 'home' } },
{ label: 'Profile', state: { page: 'profile' } },
{ label: 'About', state: { page: 'about' } }
];
let pointer = STACK.length - 1;
function logLine(text, cls) {
if (consoleEl.querySelector('.pop-console-empty')) consoleEl.innerHTML = '';
const line = document.createElement('div');
line.className = 'pop-console-line ' + (cls || '');
line.textContent = text;
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
// The exact listener from the example \u2014 genuinely registered on window.
window.addEventListener('popstate', (event) => {
console.log(event.state);
logLine('popstate fired \u25b8 handler ran', 'is-event');
logLine('event.state \u25b8 ' + JSON.stringify(event.state), 'is-good');
});
function render() {
stackRow.innerHTML = '';
STACK.forEach((entry, i) => {
if (i > 0) {
const arrow = document.createElement('span');
arrow.className = 'pop-arrow';
arrow.textContent = '\u2192';
stackRow.appendChild(arrow);
}
const el = document.createElement('div');
el.className = 'pop-entry ' + (i === pointer ? 'is-current' : 'is-visited');
el.textContent = entry.label;
stackRow.appendChild(el);
});
backBtn.disabled = pointer === 0;
fwdBtn.disabled = pointer === STACK.length - 1;
}
function navigate(delta, label) {
const target = pointer + delta;
if (target < 0 || target > STACK.length - 1) return;
pointer = target;
logLine('> ' + label + ' // triggers popstate', 'is-echo');
window.dispatchEvent(new PopStateEvent('popstate', { state: STACK[pointer].state }));
render();
}
backBtn.addEventListener('click', () => navigate(-1, 'history.back();'));
fwdBtn.addEventListener('click', () => navigate(1, 'history.forward();'));
resetBtn.addEventListener('click', () => {
pointer = STACK.length - 1;
render();
consoleEl.innerHTML = '<div class="pop-console-empty">// this is a real popstate listener \u2014 press Back to fire it</div>';
});
clearBtn.addEventListener('click', () => {
consoleEl.innerHTML = '<div class="pop-console-empty">// this is a real popstate listener \u2014 press Back to fire it</div>';
});
render();
})();
</script>β When to Use History API
When | Why |
|---|---|
Building SPA | Avoids full page reload |
Custom navigation | You control URL + UI |
Dynamic content | Helps maintain browser navigation behavior |
Scroll restoration | Remember userβs scroll position |
βοΈ Limitations
Cannot navigate to external websites using pushState.
stateobject should be small.Does not trigger
popstatewhen usingpushState()orreplaceState().
π§© History API β Example
π Project Structure
history-api-demo/
β
βββ index.html
βββ style.css
βββ script.js<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>History API Demo</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<!-- Navigation -->
<nav>
<button id="home">Home</button>
<button id="profile">Profile</button>
<button id="about">About</button>
</nav>
<!-- Content Area -->
<main id="content">
<h1>Home Page</h1>
<p>Welcome! Click the buttons above to navigate.</p>
</main>
<footer>
Try using browser Back & Forward buttons
</footer>
<script src="script.js"></script>
</body>
</html>/* ===== RESET ===== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: Arial, Helvetica, sans-serif;
}
body {
background: #f4f6f8;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
/* ===== NAVBAR ===== */
nav {
width: 100%;
background: #1e293b;
padding: 16px;
display: flex;
justify-content: center;
gap: 20px;
}
nav button {
background: #334155;
color: white;
border: none;
padding: 10px 18px;
font-size: 16px;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
}
nav button:hover {
background: #475569;
}
/* ===== CONTENT ===== */
#content {
margin-top: 60px;
padding: 40px;
background: white;
width: 90%;
max-width: 600px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
text-align: center;
}
#content h1 {
font-size: 36px;
margin-bottom: 16px;
color: #0f172a;
}
#content p {
font-size: 18px;
color: #475569;
}
/* ===== FOOTER ===== */
footer {
margin-top: 30px;
font-size: 14px;
color: #64748b;
}const content = document.getElementById("content");
function render(page) {
content.innerHTML = `
<h1>${page} Page</h1>
<p>This content was loaded without page refresh.</p>
`;
}
function navigate(page, url) {
history.pushState({ page }, "", url);
render(page);
}
// Navigation buttons
document.getElementById("home").addEventListener("click", () => {
navigate("Home", "/home");
});
document.getElementById("profile").addEventListener("click", () => {
navigate("Profile", "/profile");
});
document.getElementById("about").addEventListener("click", () => {
navigate("About", "/about");
});
// Handle browser Back & Forward buttons
window.addEventListener("popstate", (event) => {
if (event.state) {
render(event.state.page);
} else {
render("Home");
}
});<div class="infographic-spahistory">
<style>
.infographic-spahistory {
--ink: #1a1a1a;
--ink-soft: #5c5c5c;
--paper: #ffffff;
--paper-alt: #f0ece4;
--accent: #2563eb;
--accent-soft:#e3ebfd;
--line: #ddd6c9;
--good: #1a8a5f;
--mono-font: 'IBM Plex Mono', monospace;
--display-font: 'Fraunces', Georgia, serif;
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-spahistory[data-theme="dark"] {
--ink: #e8e6e1;
--ink-soft: #9a968c;
--paper: #121212;
--paper-alt: #1e1e26;
--accent: #5b8def;
--accent-soft:#202942;
--line: #2c313d;
--good: #4fbf8f;
}
.infographic-spahistory * { box-sizing: border-box; }
.infographic-spahistory h1 { font-family: var(--display-font); font-weight: 600; margin: 0; }
.infographic-spahistory p { margin: 0; }
.infographic-spahistory button { font-family: inherit; }
.sh-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.15em; }
.sh-icon svg { width: 100%; height: 100%; display: block; }
.sh-wrap { padding: clamp(1.25rem, 5vw, 2.5rem) clamp(1.1rem, 5vw, 3rem) 2rem; }
.sh-hero { text-align: center; margin-bottom: 1.3rem; }
.sh-hero h1 { font-size: clamp(1.3rem, 4.2vw, 1.75rem); }
.sh-hero p { color: var(--ink-soft); margin-top: 0.4rem; font-size: 0.86rem; }
/* --- recreated demo, literal source styling --- */
.sh-demo { border-radius: 12px; overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.12); margin: 0 auto 1rem; max-width: 560px; }
.sh-nav { width: 100%; background: #1e293b; padding: 14px; display: flex; justify-content: center; gap: 14px; flex-wrap: wrap; }
.sh-nav button { background: #334155; color: #fff; border: none; padding: 8px 16px; font-size: 0.85rem; border-radius: 6px;
cursor: pointer; transition: background 0.2s ease; font-family: Arial, Helvetica, sans-serif; }
.sh-nav button:hover { background: #475569; }
.sh-nav button.is-active { background: var(--accent); }
.sh-content-area { background: #f4f6f8; padding: 2.2rem 1.5rem; text-align: center; }
.infographic-spahistory[data-theme="dark"] .sh-content-area { background: #1a1f29; }
.sh-content-card { background: #fff; padding: 1.8rem 1.5rem; border-radius: 10px; box-shadow: 0 6px 18px rgba(0,0,0,0.08);
max-width: 420px; margin: 0 auto; font-family: Arial, Helvetica, sans-serif; }
.infographic-spahistory[data-theme="dark"] .sh-content-card { background: #23293a; }
.sh-content-card h2 { font-size: 1.5rem; margin-bottom: 0.6rem; color: #0f172a; }
.infographic-spahistory[data-theme="dark"] .sh-content-card h2 { color: #e8e6e1; }
.sh-content-card p { font-size: 0.9rem; color: #475569; }
.infographic-spahistory[data-theme="dark"] .sh-content-card p { color: #9aa4b8; }
.sh-footer { text-align: center; font-size: 0.72rem; color: #64748b; background: #f4f6f8; padding: 0 0 1rem; font-family: Arial, Helvetica, sans-serif; }
.infographic-spahistory[data-theme="dark"] .sh-footer { background: #1a1f29; color: #7d879a; }
/* --- our added back/forward controls --- */
.sh-hist-controls { display: flex; gap: 0.5rem; justify-content: center; margin-bottom: 1.2rem; }
.sh-hist-btn { display: flex; align-items: center; gap: 0.3rem; border: 1px solid var(--line); background: var(--paper);
border-radius: 6px; padding: 0.4rem 0.85rem; font-size: 0.72rem; font-weight: 600; cursor: pointer; color: var(--ink-soft); }
.sh-hist-btn:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
.sh-hist-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.sh-hist-btn .sh-icon { width: 12px; height: 12px; }
/* --- stack --- */
.sh-stack-label { text-align: center; font-family: var(--mono-font); font-size: 0.64rem; color: var(--ink-soft);
text-transform: uppercase; letter-spacing: 0.03em; margin-bottom: 0.5rem; }
.sh-stack-row { display: flex; align-items: center; justify-content: center; flex-wrap: wrap; min-height: 3rem; margin-bottom: 1.2rem; }
.sh-entry { display: flex; align-items: center; justify-content: center; padding: 0 0.65rem; height: 32px; border-radius: 6px;
font-family: var(--mono-font); font-size: 0.72rem; font-weight: 700; border: 2px solid var(--line); color: var(--ink-soft);
background: var(--paper); transition: all 0.2s ease; }
.sh-entry.is-visited { border-color: var(--accent); color: var(--accent); background: var(--accent-soft); }
.sh-entry.is-current { border-color: var(--good); color: var(--good); background: var(--paper); box-shadow: 0 0 0 3px color-mix(in srgb, var(--good) 20%, transparent); }
.sh-arrow { color: var(--ink-soft); font-size: 0.85rem; margin: 0 0.12rem; opacity: 0.6; }
/* --- console --- */
.sh-console-panel { max-width: 480px; margin: 0 auto 0; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; }
.sh-console-head { display: flex; align-items: center; justify-content: space-between; padding: 0.5rem 0.75rem;
border-bottom: 1px solid var(--line); background: var(--paper-alt); font-family: var(--mono-font); font-size: 0.7rem;
font-weight: 700; color: #888; }
.sh-clear-btn { background: none; border: none; color: var(--ink-soft); font-family: var(--mono-font); font-size: 0.64rem;
cursor: pointer; padding: 0.15rem 0.4rem; border-radius: 4px; }
.sh-clear-btn:hover { color: var(--accent); background: var(--accent-soft); }
.sh-console { min-height: 6.5rem; max-height: 9rem; overflow-y: auto; background: #1e1e1e; padding: 0.5rem 0.7rem; }
.sh-console-line { font-family: var(--mono-font); font-size: 0.66rem; color: #d4d4d4; padding: 0.08rem 0; animation: sh-pop-in 0.2s ease; white-space: pre-wrap; }
.sh-console-line.is-echo { color: #7fb0ff; }
.sh-console-line.is-event { color: #d8a657; padding-left: 0.9rem; }
.sh-console-line.is-good { color: #9fd9a8; padding-left: 0.9rem; }
.sh-console-empty { font-family: var(--mono-font); font-size: 0.66rem; color: #6b6b6b; }
@keyframes sh-pop-in { from { opacity: 0; transform: translateY(-3px); } to { opacity: 1; transform: translateY(0); } }
.sh-note { text-align: center; font-size: 0.78rem; color: var(--ink-soft); padding-top: 1.4rem; max-width: 560px; margin: 0 auto; }
.sh-note strong { color: var(--ink); }
@media (prefers-reduced-motion: reduce) {
.infographic-spahistory *, .infographic-spahistory *::before, .infographic-spahistory *::after {
animation-duration: 0.001ms !important; transition-duration: 0.001ms !important;
}
}
</style>
<div class="sh-wrap">
<div class="sh-demo">
<nav class="sh-nav">
<button id="shHome" type="button">Home</button>
<button id="shProfile" type="button">Profile</button>
<button id="shAbout" type="button">About</button>
</nav>
<div class="sh-content-area">
<div class="sh-content-card" id="shContent">
<h2>Home Page</h2>
<p>Welcome! Click the buttons above to navigate.</p>
</div>
</div>
<div class="sh-footer">Try the Back / Forward controls below</div>
</div>
<div class="sh-hist-controls">
<button class="sh-hist-btn" id="shBackBtn" type="button"><span class="sh-icon" id="shBackIcon"></span>Back</button>
<button class="sh-hist-btn" id="shFwdBtn" type="button"><span class="sh-icon" id="shFwdIcon"></span>Forward</button>
</div>
<div class="sh-stack-label">history stack (tracked as you navigate)</div>
<div class="sh-stack-row" id="shStackRow"></div>
<div class="sh-console-panel">
<div class="sh-console-head">
<span>>_ event log</span>
<button class="sh-clear-btn" id="shClearBtn" type="button">clear</button>
</div>
<div class="sh-console" id="shConsole">
<div class="sh-console-empty">// click a nav button to begin</div>
</div>
</div>
</div>
</div>
<script>
(function () {
const wrapper = document.currentScript.closest('.infographic-spahistory')
|| document.querySelector('.infographic-spahistory');
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 ICONS = {
chevLeft: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m15 18-6-6 6-6"/></svg>',
chevRight: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="m9 18 6-6-6-6"/></svg>'
};
wrapper.querySelector('#shBackIcon').innerHTML = ICONS.chevLeft;
wrapper.querySelector('#shFwdIcon').innerHTML = ICONS.chevRight;
const content = wrapper.querySelector('#shContent');
const navBtns = {
Home: wrapper.querySelector('#shHome'),
Profile: wrapper.querySelector('#shProfile'),
About: wrapper.querySelector('#shAbout')
};
const backBtn = wrapper.querySelector('#shBackBtn');
const fwdBtn = wrapper.querySelector('#shFwdBtn');
const stackRow = wrapper.querySelector('#shStackRow');
const consoleEl = wrapper.querySelector('#shConsole');
const clearBtn = wrapper.querySelector('#shClearBtn');
function logLine(text, cls) {
if (consoleEl.querySelector('.sh-console-empty')) consoleEl.innerHTML = '';
const line = document.createElement('div');
line.className = 'sh-console-line ' + (cls || '');
line.textContent = text;
consoleEl.appendChild(line);
consoleEl.scrollTop = consoleEl.scrollHeight;
}
// --- the actual app logic, matching the source verbatim ---
function render(page) {
content.innerHTML = '<h2>' + page + ' Page</h2><p>This content was loaded without page refresh.</p>';
Object.keys(navBtns).forEach(key => navBtns[key].classList.toggle('is-active', key === page));
}
function navigate(page, url) {
pushCounter += 1;
const histId = pushCounter;
try {
history.pushState({ page, __histId: histId }, '');
} catch (err) {
logLine('pushState blocked in this embed \u2014 tracking state locally instead', 'is-event');
}
logLine("> history.pushState({page: '" + page + "'}, '', '" + url + "');", 'is-echo');
render(page);
trackPush(page, histId);
}
navBtns.Home.addEventListener('click', () => navigate('Home', '/home'));
navBtns.Profile.addEventListener('click', () => navigate('Profile', '/profile'));
navBtns.About.addEventListener('click', () => navigate('About', '/about'));
window.addEventListener('popstate', (event) => {
logLine('popstate fired \u25b8 event.state = ' + JSON.stringify(event.state), 'is-event');
if (event.state) {
render(event.state.page);
} else {
render('Home');
}
trackPop(event.state);
});
// --- visualization tracking (mirrors the real stack we just created) ---
let stack = [{ label: 'Home', histId: 0 }];
let pointer = 0;
let pushCounter = 0;
function renderStack() {
stackRow.innerHTML = '';
stack.forEach((entry, i) => {
if (i > 0) {
const arrow = document.createElement('span');
arrow.className = 'sh-arrow';
arrow.textContent = '\u2192';
stackRow.appendChild(arrow);
}
const el = document.createElement('div');
el.className = 'sh-entry ' + (i === pointer ? 'is-current' : 'is-visited');
el.textContent = entry.label;
stackRow.appendChild(el);
});
backBtn.disabled = pointer === 0;
fwdBtn.disabled = pointer === stack.length - 1;
}
function trackPush(page, histId) {
stack = stack.slice(0, pointer + 1);
stack.push({ label: page, histId: histId });
pointer = stack.length - 1;
logLine('current \u2192 ' + page, 'is-good');
renderStack();
}
function trackPop(state) {
if (!state) {
pointer = 0;
} else {
const idx = stack.findIndex(e => e.histId === state.__histId);
if (idx !== -1) pointer = idx;
}
renderStack();
}
backBtn.addEventListener('click', () => history.back());
fwdBtn.addEventListener('click', () => history.forward());
clearBtn.addEventListener('click', () => {
consoleEl.innerHTML = '<div class="sh-console-empty">// click a nav button to begin</div>';
});
render('Home');
renderStack();
})();
</script>π§ How This Example Works
πΉ Clicking a button
Button Click
β pushState()
β URL changes
β render() updates content
β NO PAGE RELOADπΉ Pressing Back / Forward
Browser navigation
β popstate event fires
β event.state contains page info
β UI updates accordinglyπ§ͺ What You Should Test
Click Profile β URL becomes
/profileClick About β URL becomes
/aboutPress Back β Goes to Profile
Press Forward β Goes to About
Refresh page β stays on last URL