Animated Tabs — Free HTML CSS JS Sliding Pill Snippet
Animated Tabs with Indicator · Navigation · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="demo">
<div class="tabs" id="tabs">
<button class="tab active" onclick="switchTab(this,0)">Overview</button>
<button class="tab" onclick="switchTab(this,1)">Features</button>
<button class="tab" onclick="switchTab(this,2)">Pricing</button>
<button class="tab" onclick="switchTab(this,3)">Docs</button>
<div class="indicator" id="indicator"></div>
</div>
<div class="panels">
<div class="panel active"><h3>Product Overview</h3><p>A high-level summary of what the product does and who it's for. Start here if you're new.</p></div>
<div class="panel"><h3>Key Features</h3><p>Deep dive into individual features — integrations, automations, custom workflows, and API access.</p></div>
<div class="panel"><h3>Pricing Plans</h3><p>Compare Starter, Pro, and Team plans. All include a 14-day free trial with no credit card required.</p></div>
<div class="panel"><h3>Documentation</h3><p>Full API reference, SDK guides, tutorials, and example projects to get you up and running fast.</p></div>
</div>
</div>* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; background: #f8fafc; display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px; }
.demo { width: 100%; max-width: 520px; }
.tabs {
display: flex; position: relative;
background: #e2e8f0; border-radius: 10px;
padding: 4px; gap: 2px; margin-bottom: 20px;
}
.tab {
flex: 1; padding: 8px 10px; font-size: 13px; font-weight: 600;
color: #64748b; background: none; border: none; cursor: pointer;
border-radius: 7px; position: relative; z-index: 1;
transition: color 0.2s; font-family: inherit; white-space: nowrap;
}
.tab.active { color: #1e293b; }
.indicator {
position: absolute; top: 4px; bottom: 4px;
background: #fff; border-radius: 7px;
box-shadow: 0 1px 4px rgba(0,0,0,0.12);
transition: left 0.25s cubic-bezier(0.4,0,0.2,1), width 0.25s cubic-bezier(0.4,0,0.2,1);
pointer-events: none;
}
.panels { }
.panel { display: none; animation: fadeIn 0.2s ease; }
.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.65; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }function switchTab(btn, idx) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
btn.classList.add('active');
document.querySelectorAll('.panel')[idx].classList.add('active');
moveIndicator(btn);
}
function moveIndicator(btn) {
const tabsEl = document.getElementById('tabs');
const ind = document.getElementById('indicator');
const rect = btn.getBoundingClientRect();
const pRect = tabsEl.getBoundingClientRect();
ind.style.left = (rect.left - pRect.left) + 'px';
ind.style.width = rect.width + 'px';
}
// Init on load
const firstTab = document.querySelector('.tab.active');
if (firstTab) moveIndicator(firstTab);
window.addEventListener('resize', () => {
const active = document.querySelector('.tab.active');
if (active) moveIndicator(active);
});Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month), 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
Animated Tabs — Sliding Pill via offsetLeft/offsetWidth & Panel Active Class

Animated tabs replace the basic underline indicator of the standard tab bar with a sliding pill background that smoothly glides between tabs when clicked. The pill communicates transition direction and gives the interface a premium, physically responsive quality — for stacked tabs, see the vertical tabs; for a two-option switch, the segmented control.
The sliding indicator
A .indicator div sits inside the .tabs container with position: absolute; transition: left 0.2s, width 0.2s. moveIndicator(btn) reads the clicked tab's offsetLeft and offsetWidth — the tab's pixel position and size relative to the container — and assigns them to indicator.style.left and indicator.style.width. The CSS transition animates the pill to the new position. This correctly handles tabs of different widths.
Why offsetLeft/offsetWidth instead of a class
The indicator's position is dynamic — it depends on the exact pixel dimensions of each tab. A CSS class can't encode runtime pixel values. Using offsetLeft and offsetWidth reads the actual rendered size, so tabs with longer labels get a wider pill automatically.
Panel switching
switchTab(btn, idx) removes .active from all tabs and panels, then adds .active to the clicked tab and the panel at index idx. The panel uses a opacity + transform transition on .panel.active for a fade-in effect. The idx parameter maps each tab button to its corresponding panel by DOM order.
Adding tabs
Add a new .tab button and a matching .panel div. The JS uses querySelectorAll and numeric index — the order in the HTML determines the mapping.
The offsetLeft/offsetWidth technique
The pill indicator reads the active tab button's actual pixel position using offsetLeft and offsetWidth — values that the browser computes after layout. Setting pill.style.left = btn.offsetLeft + 'px' and pill.style.width = btn.offsetWidth + 'px' positions and sizes the pill to exactly match the active tab. CSS transition: left 0.2s, width 0.2s animates between positions. This approach handles any tab label length, any font size, and any number of tabs automatically — no hardcoded positions needed.
Panel switching
Each tab button has a data-panel attribute matching a panel ID. Clicking a tab: (1) removes .active from all tab buttons, (2) adds .active to the clicked button, (3) hides all panels, (4) shows the matching panel. The panel switch is instant (no animation) while the pill indicator slides smoothly — this matches the expected tab behaviour where content switches immediately but the indicator communicates which tab was clicked.
Accessible implementation
Add role="tablist" to the tab container, role="tab" to each button, and role="tabpanel" to each panel. Add aria-selected="true/false" to tab buttons and aria-hidden="true/false" to panels. Add aria-controls on each tab pointing to its panel ID, and aria-labelledby on each panel pointing to its tab ID.
Step by step
How to Use
- 1Click each tabClick Design, Development, and Marketing tabs to see the pill indicator slide smoothly between them and the panel content switch.
- 2Update tab labels and panel contentIn the HTML panel, change the tab button text and each .panel div content.
- 3Add a fourth tabAdd a .tab button and a matching .panel div. The JS uses index order automatically.
- 4Change animation speedUpdate 0.2s on transition: left, width in the CSS .indicator rule.
- 5Change indicator colourUpdate background: #fff on .indicator to any colour.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
moveIndicator(btn) reads btn.offsetLeft (the tab's left pixel position relative to .tabs) and btn.offsetWidth (the tab's width). It sets indicator.style.left and indicator.style.width to these values. CSS transition: left 0.2s, width 0.2s animates between positions.
The indicator position needs to match the exact pixel location and size of each tab — values that depend on the rendered tab label length. CSS classes cannot encode runtime pixel values. offsetLeft and offsetWidth read the actual DOM geometry.
Add a <button class="tab" onclick="switchTab(this, N)">Label</button> where N is the zero-based panel index. Add a matching <div class="panel">content</div>. The querySelectorAll picks up new tabs automatically.
Call switchTab(document.querySelectorAll(".tab")[N], N) after the JS code to open the Nth tab on page load.
Yes. Click "JSX" for a React component. In React, manage activeTab in useState. Use useRef on the indicator element and read the active tab button's offsetLeft/offsetWidth in a useEffect after state changes to update the indicator position.
The Tab Bar uses a CSS border-bottom underline indicator. Animated Tabs uses a sliding pill background. The pill is more visually prominent and communicates transition direction. The panel switching logic is similar in both.
Yes. While the Animated Tabs with Indicator 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.