Skip to content

Instantly share code, notes, and snippets.

@zilveer
Created September 28, 2025 02:01
Show Gist options
  • Select an option

  • Save zilveer/8fa4c57d5c3b358952651b9aad32543b to your computer and use it in GitHub Desktop.

Select an option

Save zilveer/8fa4c57d5c3b358952651b9aad32543b to your computer and use it in GitHub Desktop.
Nested swipeable tabs demo
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nested Tabs with Swipe Panels</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.tabs-container {
background: white;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0,0,0,0.1);
overflow: hidden;
margin-bottom: 20px;
}
.tabs-nav {
display: flex;
background: #f8f9fa;
border-bottom: 1px solid #e9ecef;
position: relative;
overflow-x: auto;
overflow-y: hidden;
scroll-behavior: smooth;
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.tabs-nav::-webkit-scrollbar {
display: none; /* Chrome/Safari */
}
.tab-button {
flex: 0 0 auto;
min-width: 120px;
padding: 16px 20px;
border: none;
background: none;
cursor: pointer;
font-size: 14px;
font-weight: 500;
color: #6c757d;
transition: all 0.3s ease;
position: relative;
white-space: nowrap;
}
.tab-button:hover {
color: #495057;
background: rgba(0,0,0,0.05);
}
.tab-button.active {
color: #007bff;
}
.tab-indicator {
position: absolute;
bottom: 0;
height: 3px;
background: #007bff;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
border-radius: 3px 3px 0 0;
}
.panels-container {
position: relative;
overflow: hidden;
}
.panel {
position: absolute;
top: 0;
left: 0;
width: 100%;
padding: 24px;
opacity: 0;
transform: translateX(30px);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
pointer-events: none;
min-height: 200px;
}
.panel.active {
position: relative;
opacity: 1;
transform: translateX(0);
pointer-events: all;
}
.panel h3 {
margin-bottom: 16px;
color: #343a40;
}
.panel p {
color: #6c757d;
line-height: 1.6;
margin-bottom: 12px;
}
/* Nested tabs styling */
.nested-tabs {
margin-top: 20px;
}
.nested-tabs .tabs-nav {
background: #f1f3f4;
}
.nested-tabs .tab-button {
font-size: 13px;
padding: 12px 16px;
min-width: 100px;
}
.nested-tabs .tab-indicator {
background: #28a745;
}
.nested-tabs .tab-button.active {
color: #28a745;
}
/* Touch/swipe support */
.swipeable {
touch-action: pan-y;
user-select: none;
}
/* Demo controls */
.demo-controls {
margin-bottom: 20px;
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.demo-controls button {
padding: 10px 16px;
border: 1px solid #ddd;
background: white;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
transition: all 0.2s;
}
.demo-controls button:hover {
background: #f8f9fa;
border-color: #007bff;
}
@media (max-width: 768px) {
.tab-button {
min-width: 100px;
padding: 12px 16px;
}
.nested-tabs .tab-button {
min-width: 80px;
padding: 10px 12px;
}
}
</style>
</head>
<body>
<div class="demo-controls">
<button onclick="addDynamicTab()">Add Dynamic Tab</button>
<button onclick="removeDynamicTab()">Remove Last Tab</button>
<button onclick="loadFromDOM()">Load Nested Tabs</button>
<button onclick="loadParentTabsFromDOM()">Load Parent Tabs from DOM</button>
<button onclick="addNestedDynamicTab()">Add Nested Dynamic Tab</button>
<button onclick="showExamples()">Show Code Examples</button>
</div>
<!-- Main tabs container -->
<div id="mainTabs" class="tabs-container" data-tabs-group="main">
<div class="tabs-nav">
<button class="tab-button active" data-tab="overview">Overview</button>
<button class="tab-button" data-tab="features">Features</button>
<button class="tab-button" data-tab="settings">Settings</button>
<button class="tab-button" data-tab="analytics">Analytics</button>
<button class="tab-button" data-tab="reports">Reports</button>
<button class="tab-button" data-tab="users">Users</button>
<button class="tab-button" data-tab="admin">Admin</button>
</div>
<div class="panels-container swipeable">
<div class="panel active" data-panel="overview">
<h3>Overview</h3>
<p>This is the overview panel with some placeholder content. You can swipe left/right on mobile devices to navigate between panels.</p>
<p>The tabs system supports nested structures and dynamic content loading.</p>
</div>
<div class="panel" data-panel="features">
<h3>Features</h3>
<p>Key features of this tab system:</p>
<p>β€’ Event delegation for efficient event handling</p>
<p>β€’ Swipe gesture support for mobile devices</p>
<p>β€’ Smooth animations and transitions</p>
<p>β€’ Dynamic tab addition/removal</p>
<p>β€’ Nested tabs support</p>
<p>β€’ Horizontally scrollable tabs with auto-centering</p>
</div>
<div class="panel" data-panel="settings">
<h3>Settings</h3>
<p>Settings panel content goes here. This demonstrates how content can be organized in different tabs.</p>
<p>Each panel can contain any type of content including forms, images, or even nested tab structures.</p>
</div>
<div class="panel" data-panel="analytics">
<h3>Analytics</h3>
<p>Analytics dashboard with charts and metrics.</p>
<p>This tab demonstrates horizontal scrolling when there are many tabs.</p>
</div>
<div class="panel" data-panel="reports">
<h3>Reports</h3>
<p>Comprehensive reporting tools and data visualization.</p>
<p>Notice how the active tab centers itself in the navigation bar.</p>
</div>
<div class="panel" data-panel="users">
<h3>Users</h3>
<p>User management and administration panel.</p>
<p>The tab navigation smoothly scrolls to keep the active tab visible.</p>
</div>
<div class="panel" data-panel="admin">
<h3>Admin</h3>
<p>System administration and configuration settings.</p>
<p>This is the last tab to demonstrate the scrolling behavior.</p>
</div>
</div>
</div>
<!-- Hidden template for nested tabs (loaded via DOM) -->
<div id="nestedTemplate" style="display: none;">
<div class="tabs-container nested-tabs" data-tabs-group="nested">
<div class="tabs-nav">
<button class="tab-button active" data-tab="general">General</button>
<button class="tab-button" data-tab="advanced">Advanced</button>
<button class="tab-button" data-tab="security">Security</button>
</div>
<div class="panels-container swipeable">
<div class="panel active" data-panel="general">
<h3>General Settings</h3>
<p>General configuration options and preferences.</p>
<p><em>Loaded from DOM template!</em></p>
</div>
<div class="panel" data-panel="advanced">
<h3>Advanced Settings</h3>
<p>Advanced configuration for power users.</p>
<p><em>This nested tab structure was loaded from a hidden DOM template.</em></p>
</div>
<div class="panel" data-panel="security">
<h3>Security Settings</h3>
<p>Security and privacy related configurations.</p>
<p><em>Each nested tab maintains its own state independently.</em></p>
</div>
</div>
</div>
</div>
<!-- Hidden template for parent tabs (loaded via DOM) -->
<div id="parentTabsTemplate" style="display: none;">
<div class="tabs-container" data-tabs-group="loaded-parent">
<div class="tabs-nav">
<button class="tab-button active" data-tab="dashboard">Dashboard</button>
<button class="tab-button" data-tab="projects">Projects</button>
<button class="tab-button" data-tab="team">Team</button>
<button class="tab-button" data-tab="billing">Billing</button>
</div>
<div class="panels-container swipeable">
<div class="panel active" data-panel="dashboard">
<h3>Dashboard</h3>
<p>Main dashboard with key metrics and overview.</p>
<p><strong>This entire tab structure was loaded from DOM!</strong></p>
<p>You can click through these tabs and they work perfectly.</p>
</div>
<div class="panel" data-panel="projects">
<h3>Projects</h3>
<p>Project management and tracking interface.</p>
<p>All tabs support swipe gestures and auto-centering.</p>
</div>
<div class="panel" data-panel="team">
<h3>Team</h3>
<p>Team collaboration and member management.</p>
<p>Event delegation handles all interactions automatically.</p>
</div>
<div class="panel" data-panel="billing">
<h3>Billing</h3>
<p>Subscription and payment management.</p>
<p>Perfect for loading complex tab structures at runtime!</p>
</div>
</div>
</div>
</div>
<!-- Container for dynamically loaded content -->
<div id="dynamicContainer"></div>
<script>
class TabsManager {
constructor() {
this.activeGroups = new Map();
this.swipeThreshold = 50;
this.swipeStartX = 0;
this.init();
}
init() {
// Event delegation for all tab interactions
document.addEventListener('click', this.handleTabClick.bind(this));
document.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: true });
document.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: true });
// Initialize existing tab groups
this.initializeTabGroups();
}
initializeTabGroups() {
const tabGroups = document.querySelectorAll('[data-tabs-group]');
tabGroups.forEach(group => {
const groupId = group.dataset.tabsGroup;
const activeTab = group.querySelector('.tab-button.active');
if (activeTab) {
this.activeGroups.set(groupId, activeTab.dataset.tab);
}
this.updateTabIndicator(group);
this.centerActiveTab(group);
});
}
handleTabClick(e) {
const tabButton = e.target.closest('.tab-button');
if (!tabButton) return;
e.preventDefault();
const container = tabButton.closest('[data-tabs-group]');
const groupId = container.dataset.tabsGroup;
const tabId = tabButton.dataset.tab;
this.activateTab(container, tabId);
this.activeGroups.set(groupId, tabId);
}
activateTab(container, tabId) {
// Update buttons
const buttons = container.querySelectorAll('.tab-button');
buttons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.tab === tabId);
});
// Update panels - use direct child selector to avoid nested panels
const panelsContainer = container.querySelector('.panels-container');
const panels = panelsContainer.children;
Array.from(panels).forEach(panel => {
if (panel.classList.contains('panel')) {
panel.classList.toggle('active', panel.dataset.panel === tabId);
}
});
this.updateTabIndicator(container);
this.centerActiveTab(container);
}
addTab(containerId, tabId, tabLabel, panelContent) {
const container = document.getElementById(containerId);
if (!container) return;
this.addTabToContainer(container, tabId, tabLabel, panelContent);
}
updateTabIndicator(container) {
const activeButton = container.querySelector('.tab-button.active');
let indicator = container.querySelector('.tab-indicator');
if (!indicator) {
indicator = document.createElement('div');
indicator.className = 'tab-indicator';
container.querySelector('.tabs-nav').appendChild(indicator);
}
if (activeButton) {
const rect = activeButton.getBoundingClientRect();
const navRect = container.querySelector('.tabs-nav').getBoundingClientRect();
indicator.style.left = (activeButton.offsetLeft) + 'px';
indicator.style.width = activeButton.offsetWidth + 'px';
}
}
centerActiveTab(container) {
const activeButton = container.querySelector('.tab-button.active');
const tabsNav = container.querySelector('.tabs-nav');
if (!activeButton || !tabsNav) return;
const navWidth = tabsNav.offsetWidth;
const navScrollWidth = tabsNav.scrollWidth;
// Only scroll if content overflows
if (navScrollWidth <= navWidth) return;
const buttonLeft = activeButton.offsetLeft;
const buttonWidth = activeButton.offsetWidth;
const buttonCenter = buttonLeft + (buttonWidth / 2);
// Calculate scroll position to center the active tab
const targetScrollLeft = buttonCenter - (navWidth / 2);
// Constrain scroll within bounds
const maxScrollLeft = navScrollWidth - navWidth;
const finalScrollLeft = Math.max(0, Math.min(targetScrollLeft, maxScrollLeft));
tabsNav.scrollTo({
left: finalScrollLeft,
behavior: 'smooth'
});
}
handleTouchStart(e) {
const swipeableArea = e.target.closest('.swipeable');
if (!swipeableArea) return;
this.swipeStartX = e.touches[0].clientX;
this.currentSwipeContainer = swipeableArea.closest('[data-tabs-group]');
}
handleTouchEnd(e) {
if (!this.currentSwipeContainer) return;
const swipeEndX = e.changedTouches[0].clientX;
const swipeDistance = this.swipeStartX - swipeEndX;
if (Math.abs(swipeDistance) > this.swipeThreshold) {
// Only get direct child tab buttons, not nested ones
const tabsNav = this.currentSwipeContainer.querySelector(':scope > .tabs-nav');
const buttons = tabsNav.querySelectorAll('.tab-button');
const activeIndex = Array.from(buttons).findIndex(btn => btn.classList.contains('active'));
let newIndex;
if (swipeDistance > 0 && activeIndex < buttons.length - 1) {
// Swipe left - next tab
newIndex = activeIndex + 1;
} else if (swipeDistance < 0 && activeIndex > 0) {
// Swipe right - previous tab
newIndex = activeIndex - 1;
}
if (newIndex !== undefined) {
const newTab = buttons[newIndex].dataset.tab;
this.activateTab(this.currentSwipeContainer, newTab);
this.activeGroups.set(this.currentSwipeContainer.dataset.tabsGroup, newTab);
}
}
this.currentSwipeContainer = null;
}
addTabToContainer(container, tabId, tabLabel, panelContent) {
// Add tab button
const tabsNav = container.querySelector('.tabs-nav');
const newButton = document.createElement('button');
newButton.className = 'tab-button';
newButton.dataset.tab = tabId;
newButton.textContent = tabLabel;
// Insert before indicator if it exists
const indicator = tabsNav.querySelector('.tab-indicator');
if (indicator) {
tabsNav.insertBefore(newButton, indicator);
} else {
tabsNav.appendChild(newButton);
}
// Add panel
const panelsContainer = container.querySelector('.panels-container');
const newPanel = document.createElement('div');
newPanel.className = 'panel';
newPanel.dataset.panel = tabId;
newPanel.innerHTML = panelContent;
panelsContainer.appendChild(newPanel);
this.updateTabIndicator(container);
this.centerActiveTab(container);
}
removeTab(containerId, tabId) {
const container = document.getElementById(containerId);
if (!container) return;
const button = container.querySelector(`[data-tab="${tabId}"]`);
const panel = container.querySelector(`[data-panel="${tabId}"]`);
if (button) button.remove();
if (panel) panel.remove();
// If removed tab was active, activate first available tab
if (button && button.classList.contains('active')) {
const firstButton = container.querySelector('.tab-button');
if (firstButton) {
this.activateTab(container, firstButton.dataset.tab);
}
}
this.updateTabIndicator(container);
}
loadFromDOM(sourceId, targetId) {
const source = document.getElementById(sourceId);
const target = document.getElementById(targetId);
if (!source || !target) return;
const clonedContent = source.cloneNode(true);
clonedContent.style.display = 'block';
clonedContent.id = '';
target.appendChild(clonedContent);
this.initializeTabGroups();
}
}
// Initialize the tabs manager
const tabsManager = new TabsManager();
// Demo functions
let dynamicTabCount = 0;
let nestedDynamicTabCount = 0;
// 1. ADD DYNAMIC PARENT TAB
function addDynamicTab() {
dynamicTabCount++;
const tabId = `dynamic-${dynamicTabCount}`;
const content = `
<h3>Dynamic Tab ${dynamicTabCount}</h3>
<p>This tab was added dynamically at runtime.</p>
<p>Tab ID: ${tabId}</p>
<p>Created at: ${new Date().toLocaleTimeString()}</p>
<p><strong>Added via JavaScript:</strong></p>
<pre style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 12px;">
tabsManager.addTab('mainTabs', '${tabId}', 'Dynamic ${dynamicTabCount}', content);
</pre>
`;
tabsManager.addTab('mainTabs', tabId, `Dynamic ${dynamicTabCount}`, content);
}
// 2. REMOVE DYNAMIC TAB
function removeDynamicTab() {
if (dynamicTabCount > 0) {
const tabId = `dynamic-${dynamicTabCount}`;
tabsManager.removeTab('mainTabs', tabId);
dynamicTabCount--;
}
}
// 3. LOAD NESTED TABS FROM DOM
function loadFromDOM() {
const settingsPanel = document.querySelector('[data-panel="settings"]');
// Check if nested tabs already exist
if (settingsPanel.querySelector('.nested-tabs')) {
alert('Nested tabs already loaded!');
return;
}
// Clone the template and append to settings panel
const template = document.getElementById('nestedTemplate');
const clonedContent = template.cloneNode(true);
clonedContent.style.display = 'block';
clonedContent.id = '';
settingsPanel.appendChild(clonedContent);
tabsManager.initializeTabGroups();
}
// 4. LOAD PARENT TABS FROM DOM
function loadParentTabsFromDOM() {
const container = document.getElementById('dynamicContainer');
// Clear existing content
container.innerHTML = '';
// Clone the parent tabs template
const template = document.getElementById('parentTabsTemplate');
const clonedContent = template.cloneNode(true);
clonedContent.style.display = 'block';
clonedContent.id = '';
container.appendChild(clonedContent);
tabsManager.initializeTabGroups();
}
// 5. ADD DYNAMIC NESTED TAB
function addNestedDynamicTab() {
const nestedContainer = document.querySelector('[data-tabs-group="nested"]');
if (!nestedContainer) {
alert('Please load nested tabs first!');
return;
}
nestedDynamicTabCount++;
const tabId = `nested-dynamic-${nestedDynamicTabCount}`;
const content = `
<h3>Nested Dynamic ${nestedDynamicTabCount}</h3>
<p>This nested tab was added dynamically!</p>
<p>Nested tabs work independently from parent tabs.</p>
`;
// Add to the nested container
tabsManager.addTab(nestedContainer.closest('[data-tabs-group]').parentNode.id || 'nested-container', tabId, `Nested ${nestedDynamicTabCount}`, content);
// Since we don't have direct container ID, we'll use the class-based approach
tabsManager.addTabToContainer(nestedContainer, tabId, `Nested ${nestedDynamicTabCount}`, content);
}
// 6. SHOW CODE EXAMPLES
function showExamples() {
const exampleContent = `
<h3>πŸ“š Code Examples</h3>
<h4>🎯 1. Add Dynamic Tab</h4>
<pre style="background: #f8f9fa; padding: 15px; border-radius: 6px; overflow-x: auto;">
// Add a new tab dynamically
tabsManager.addTab(
'containerId', // Container ID
'newTabId', // Unique tab ID
'Tab Label', // Display name
'&lt;h3&gt;Content&lt;/h3&gt;' // HTML content
);
</pre>
<h4>πŸ—‘οΈ 2. Remove Tab</h4>
<pre style="background: #f8f9fa; padding: 15px; border-radius: 6px; overflow-x: auto;">
// Remove a tab
tabsManager.removeTab('containerId', 'tabId');
</pre>
<h4>πŸ“‹ 3. Load from DOM Template</h4>
<pre style="background: #f8f9fa; padding: 15px; border-radius: 6px; overflow-x: auto;">
// HTML Template (hidden)
&lt;div id="myTemplate" style="display: none;"&gt;
&lt;div class="tabs-container" data-tabs-group="myGroup"&gt;
&lt;div class="tabs-nav"&gt;
&lt;button class="tab-button active" data-tab="tab1"&gt;Tab 1&lt;/button&gt;
&lt;/div&gt;
&lt;div class="panels-container swipeable"&gt;
&lt;div class="panel active" data-panel="tab1"&gt;Content&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
// JavaScript to load
const template = document.getElementById('myTemplate');
const clone = template.cloneNode(true);
clone.style.display = 'block';
clone.id = '';
targetContainer.appendChild(clone);
tabsManager.initializeTabGroups();
</pre>
<h4>πŸ”§ 4. Direct Container Method</h4>
<pre style="background: #f8f9fa; padding: 15px; border-radius: 6px; overflow-x: auto;">
// Add tab to specific container element
const container = document.querySelector('[data-tabs-group="myGroup"]');
tabsManager.addTabToContainer(container, 'tabId', 'Label', 'Content');
</pre>
<h4>🎨 5. Create Complete Structure Dynamically</h4>
<pre style="background: #f8f9fa; padding: 15px; border-radius: 6px; overflow-x: auto;">
// Create a complete tab structure
const tabStructure = \`
&lt;div class="tabs-container" data-tabs-group="dynamic"&gt;
&lt;div class="tabs-nav"&gt;
&lt;button class="tab-button active" data-tab="home"&gt;Home&lt;/button&gt;
&lt;button class="tab-button" data-tab="about"&gt;About&lt;/button&gt;
&lt;/div&gt;
&lt;div class="panels-container swipeable"&gt;
&lt;div class="panel active" data-panel="home"&gt;Home Content&lt;/div&gt;
&lt;div class="panel" data-panel="about"&gt;About Content&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;\`;
document.body.insertAdjacentHTML('beforeend', tabStructure);
tabsManager.initializeTabGroups();
</pre>
<h4>πŸ“± Key Features Available:</h4>
<ul style="margin: 15px 0;">
<li>βœ… Event delegation (no need to rebind events)</li>
<li>βœ… Auto-centering active tabs</li>
<li>βœ… Swipe gesture support</li>
<li>βœ… Smooth animations</li>
<li>βœ… Nested tab support</li>
<li>βœ… Dynamic add/remove</li>
<li>βœ… Horizontal scrolling</li>
</ul>
`;
// Create or update examples tab
const existingPanel = document.querySelector('[data-panel="examples"]');
if (existingPanel) {
existingPanel.innerHTML = exampleContent;
tabsManager.activateTab(document.getElementById('mainTabs'), 'examples');
} else {
tabsManager.addTab('mainTabs', 'examples', 'πŸ“š Examples', exampleContent);
tabsManager.activateTab(document.getElementById('mainTabs'), 'examples');
}
}
// Handle window resize for tab indicators and centering
window.addEventListener('resize', () => {
const tabGroups = document.querySelectorAll('[data-tabs-group]');
tabGroups.forEach(group => {
tabsManager.updateTabIndicator(group);
tabsManager.centerActiveTab(group);
});
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment