Tabs with URL Sync — Free HTML CSS JS Hash-Linked Tab Bar Snippet
Tabs with URL Sync · Navigation · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="tabs" role="tablist" aria-label="URL synced tabs">
<button class="tab" data-tab="overview" onclick="goToTab('overview')">Overview</button>
<button class="tab" data-tab="pricing" onclick="goToTab('pricing')">Pricing</button>
<button class="tab" data-tab="faq" onclick="goToTab('faq')">FAQ</button>
</div>
<div class="panels">
<div class="panel" data-panel="overview">
<h3>Overview</h3>
<p>This tab's state lives in the URL hash. Try refreshing the page after switching tabs.</p>
</div>
<div class="panel" data-panel="pricing">
<h3>Pricing</h3>
<p>Share a link straight to this tab — <code>#pricing</code> opens it automatically on load.</p>
</div>
<div class="panel" data-panel="faq">
<h3>FAQ</h3>
<p>Browser back/forward buttons also switch tabs, since each click pushes a new hash entry.</p>
</div>
</div>
<p class="hash-hint">Current hash: <code id="hashDisplay">#overview</code></p>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f8fafc; padding: 24px; display: flex; align-items: center; flex-direction: column; gap: 20px; justify-content: center; min-height: 100vh; }
.tabs {
display: flex;
gap: 2px;
border-bottom: 2px solid #e2e8f0;
margin-bottom: 20px;
}
.tab {
padding: 8px 18px;
font-size: 14px;
font-weight: 500;
color: #64748b;
background: none;
border: none;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -2px;
transition: color 0.15s, border-color 0.15s;
border-radius: 4px 4px 0 0;
}
.tab:hover { color: #1e293b; background: #f1f5f9; }
.tab.active { color: #6366f1; border-bottom-color: #6366f1; font-weight: 600; }
.panel { display: none; }
.panel.active { display: block; }
.panel h3 { font-size: 16px; font-weight: 700; color: #1e293b; margin-bottom: 8px; }
.panel p { font-size: 14px; color: #64748b; line-height: 1.6; }
.panel code { background: #f1f5f9; padding: 2px 6px; border-radius: 4px; font-size: 13px; }
.hash-hint { margin-top: 20px; font-size: 13px; color: #94a3b8; }
.hash-hint code { background: #eef2ff; color: #4f46e5; padding: 2px 8px; border-radius: 4px; font-weight: 600; }function goToTab(tabName) {
location.hash = tabName;
}
function activateTab(tabName) {
const valid = ['overview', 'pricing', 'faq'];
const name = valid.includes(tabName) ? tabName : 'overview';
document.querySelectorAll('.tab').forEach(t => {
t.classList.toggle('active', t.dataset.tab === name);
});
document.querySelectorAll('.panel').forEach(p => {
p.classList.toggle('active', p.dataset.panel === name);
});
const display = document.getElementById('hashDisplay');
if (display) display.textContent = '#' + name;
}
function readHashAndActivate() {
const tabName = location.hash.replace('#', '');
activateTab(tabName);
}
window.addEventListener('hashchange', readHashAndActivate);
readHashAndActivate();Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Tabs with URL Sync — HTML, CSS & JavaScript Hash-Based Tab State

Ordinary tab bars lose their state on refresh — click "Pricing", hit F5, and you're back on the first tab. This snippet fixes that by storing the active tab in the URL hash (location.hash), so the browser itself remembers which tab was open. Refreshing, bookmarking, or sending a colleague a link to webdevpuneet.com/page#pricing all open the correct panel.
How the hash becomes the source of truth
Clicking a tab doesn't directly toggle classes — it calls goToTab(tabName), which simply sets location.hash = tabName. Setting the hash is what triggers the browser's native hashchange event, which fires a listener calling readHashAndActivate(). That function reads location.hash, strips the leading #, and calls activateTab(name) to update the visible tab and panel. The result: the hash change is the only thing that ever changes the visible tab — clicking a tab bar button and typing a hash directly into the address bar go through the exact same code path.
How back/forward buttons work for free
Every time location.hash is set to a new value, the browser pushes a new entry onto the session history. That means the native Back and Forward buttons walk through your tab history automatically — no manual history.pushState bookkeeping required. This is one of the main advantages of hash-based state over a plain JS variable.
How the initial load is handled
On first load there may be no hash at all (a fresh visit to the page) or an invalid one (a stale bookmark). activateTab guards against this with a valid.includes(tabName) check, falling back to 'overview' if the hash is missing or unrecognized. readHashAndActivate() is called once immediately on script load, in addition to being wired to the hashchange event, so the correct tab is shown even before the user clicks anything.
Extending to more tabs
Add the tab name to the valid array, add a matching data-tab/data-panel pair in the HTML, and the hash-driven activation continues to work with no other logic changes.
Step by step
How to Use
- 1Load the snippetClick "Tabs with URL Sync" in the sidebar Library tab to load the panels and preview.
- 2Click a tab and check the hashClick Pricing or FAQ — notice the "Current hash" text and the browser URL bar (in the exported HTML file) both update.
- 3Test refresh persistenceExport to an HTML file, open it, switch tabs, and refresh — the same tab reopens because the hash survives a reload.
- 4Test back/forwardClick through a couple of tabs then press the browser Back button — it steps back through your tab history.
- 5Add a tabAdd the new name to the valid array in JS and add matching data-tab/data-panel elements in HTML.
- 6Export and saveUse the export buttons for a standalone file or component, or click "Save as" to keep your version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
A plain variable resets on every page load. location.hash is part of the URL, so it survives refreshes, gets included when the link is shared or bookmarked, and integrates with the browser's native back/forward history for free.
No. Setting location.hash does not trigger a network request or full page reload — it only fires the hashchange event, which the script listens for to update the visible tab.
activateTab checks the requested tab name against a valid array. If it is not recognized (or the hash is empty), it falls back to showing the default "overview" tab instead of showing nothing.
Every hash assignment pushes a new browser history entry automatically. The Back and Forward buttons move through those entries, which re-fires hashchange and re-activates the corresponding tab — no extra code needed.
Yes, conceptually. Replace location.hash assignments with history.pushState calls and listen for the popstate event instead of hashchange, then read location.pathname instead of location.hash.
Add its name to the valid array in the JS panel, then add a matching button with data-tab="name" and a panel with data-panel="name" in the HTML panel.
No — like any dynamically-toggled tab pattern, it requires JavaScript to read the hash and toggle the active classes. Without JS, no panel would ever become visible.
Yes. While the Tabs with URL Sync snippet gives you one pattern to copy, the free Responsive Navbar Builder at webdevpuneet.com/navbar-builder/ designs a whole navigation bar: logo, links and dropdown menus, a mobile hamburger menu (dropdown panel, slide-in drawer or full screen) and a scroll effect (sticky, shrink, hide on scroll or transparent). It has six presets, a live preview that runs the real code, and exports HTML + CSS + vanilla JS, React, Tailwind, Vue 3 or Angular — or you can click Fork & Edit to keep editing it in My Code.