Tabs
Organize related content into separate panels with smooth transitions and keyboard navigation
Overview
Tabs organise related content into separate panels that users can switch between by clicking tab headers. They're perfect for displaying large amounts of information in a compact, organised way without overwhelming users.
Key Features
- Multiple visual styles (standard, pills, underline)
- Smooth animated transitions between panels
- Programmatic control (show, next, prev)
- Keyboard navigation (arrow keys, Home, End)
- Event callbacks for tab changes
- Automatic ARIA attributes for accessibility
- Responsive behaviour on mobile devices
When to Use Tabs
- Content Organisation: Product details, documentation, settings panels
- Alternative Views: Charts vs tables, grid vs list, different time periods
- Forms: Multi-step wizards, categorised form sections
- Dashboards: Different data views, analytics categories
- Navigation: Profile sections (about, posts, photos), account settings
Basic Usage
// HTML structure
<div id="my-tabs" class="tabs">
<ul class="tab-list">
<li class="tab-item active">Tab 1</li>
<li class="tab-item">Tab 2</li>
<li class="tab-item">Tab 3</li>
</ul>
<div class="tab-content">
<div class="tab-panel active">Panel 1 content</div>
<div class="tab-panel">Panel 2 content</div>
<div class="tab-panel">Panel 3 content</div>
</div>
</div>
// JavaScript
const tabs = Domma.elements.tabs('#my-tabs', {
activeIndex: 0,
animation: true
});
// Programmatic control
tabs.show(1); // Show second tab
tabs.next(); // Go to next tab
tabs.prev(); // Go to previous tab
tabs.getActive(); // Returns active index
Quick Start
Create basic tabs in under a minute. Tabs work automatically with the correct HTML structure.
Simple Tabs
// HTML
<div id="quick-tabs" class="tabs">
<ul class="tab-list">
<li class="tab-item active">Home</li>
<li class="tab-item">Profile</li>
<li class="tab-item">Messages</li>
</ul>
<div class="tab-content">
<div class="tab-panel active">
<h3>Home</h3>
<p>Welcome to your dashboard.</p>
</div>
<div class="tab-panel">
<h3>Profile</h3>
<p>Your profile information.</p>
</div>
<div class="tab-panel">
<h3>Messages</h3>
<p>You have 3 new messages.</p>
</div>
</div>
</div>
// JavaScript
Domma.elements.tabs('#quick-tabs');
Home
Welcome to your dashboard. Click the tabs above to navigate between sections.
Profile
Your profile information goes here. Edit your details and preferences.
Messages
You have 3 new messages. Click to view your conversations.
<div id="quick-tabs" class="tabs">
<div class="tab-list" role="tablist">
<button type="button" class="tab-item active" role="tab">Home</button>
<button type="button" class="tab-item" role="tab">Profile</button>
<button type="button" class="tab-item" role="tab">Messages</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel">
<h3>Home</h3>
<p>Welcome to your dashboard. Click the tabs above to navigate between sections.</p>
</div>
<div class="tab-panel" role="tabpanel">
<h3>Profile</h3>
<p>Your profile information goes here. Edit your details and preferences.</p>
</div>
<div class="tab-panel" role="tabpanel">
<h3>Messages</h3>
<p>You have 3 new messages. Click to view your conversations.</p>
</div>
</div>
</div>/**
* Simple Tabs - from public/showcase/elements/tabs.
*/
export function mount(root) {
E.tabs(root.querySelector('#quick-tabs'));
}Step-by-Step Tutorial
Build a product information page with tabs for Overview, Features, and Reviews.
Step 1: Create Tab Headers
Start with the tab list structure:
<div id="product-tabs" class="tabs">
<ul class="tab-list">
<li class="tab-item active">Overview</li>
<li class="tab-item">Features</li>
<li class="tab-item">Reviews</li>
</ul>
</div>
Step 2: Add Tab Panels
Add corresponding content panels (same order as tabs):
<div id="product-tabs" class="tabs">
<ul class="tab-list">...</ul>
<div class="tab-content">
<div class="tab-panel active">
<h3>Product Overview</h3>
<p>Description of the product...</p>
</div>
<div class="tab-panel">
<h3>Key Features</h3>
<ul>
<li>Feature 1</li>
<li>Feature 2</li>
</ul>
</div>
<div class="tab-panel">
<h3>Customer Reviews</h3>
<p>⭐⭐⭐⭐⭐ (4.8/5)</p>
</div>
</div>
</div>
Step 3: Initialise with Animation
Add smooth transitions between tabs:
const productTabs = Domma.elements.tabs('#product-tabs', {
activeIndex: 0, // Start with first tab
animation: true // Enable smooth transitions
});
Step 4: Add Change Callback
Track which tab users view:
const productTabs = Domma.elements.tabs('#product-tabs', {
activeIndex: 0,
animation: true,
onChange: (index, panel) => {
const tabs = ['Overview', 'Features', 'Reviews'];
console.log(`User viewing: ${tabs[index]}`);
}
});
Step 5: Add Next/Previous Buttons
Navigate programmatically with buttons:
<button id="prev-tab">← Previous</button>
<button id="next-tab">Next →</button>
$('#prev-tab').on('click', () => {
productTabs.prev();
});
$('#next-tab').on('click', () => {
productTabs.next();
});
Step 6: Complete Working Demo
Product Overview
Premium wireless headphones with industry-leading noise cancellation technology.
Experience crystal-clear audio quality and all-day comfort.
Key Features
- Active Noise Cancellation (ANC)
- 30-hour battery life
- Premium leather cushions
- Bluetooth 5.0 connectivity
- Foldable design with carry case
Customer Reviews
⭐⭐⭐⭐⭐
4.8 out of 5 (1,247 reviews)
"Best headphones I've ever owned!" - Sarah M.
<div id="tutorial-tabs" class="tabs mb-3">
<div class="tab-list" role="tablist">
<button type="button" class="tab-item active" role="tab">Overview</button>
<button type="button" class="tab-item" role="tab">Features</button>
<button type="button" class="tab-item" role="tab">Reviews</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel">
<h3>Product Overview</h3>
<p class="mb-2">Premium wireless headphones with industry-leading noise cancellation
technology.</p>
<p>Experience crystal-clear audio quality and all-day comfort.</p>
</div>
<div class="tab-panel" role="tabpanel">
<h3>Key Features</h3>
<ul>
<li>Active Noise Cancellation (ANC)</li>
<li>30-hour battery life</li>
<li>Premium leather cushions</li>
<li>Bluetooth 5.0 connectivity</li>
<li>Foldable design with carry case</li>
</ul>
</div>
<div class="tab-panel" role="tabpanel">
<h3>Customer Reviews</h3>
<p class="text-2xl text-warning">⭐⭐⭐⭐⭐</p>
<p class="mb-2"><strong>4.8 out of 5</strong> (1,247 reviews)</p>
<p class="text-sm">"Best headphones I've ever owned!" - Sarah M.</p>
</div>
</div>
</div>
<div class="flex gap-2">
<button type="button" class="btn btn-secondary" id="tutorial-prev">← Previous</button>
<button type="button" class="btn btn-secondary" id="tutorial-next">Next →</button>
<div class="demo-result flex-1 tutorial-result-tabs" id="tutorial-result"></div>
</div>/**
* Step 6: Complete Working Demo - from public/showcase/elements/tabs.
*/
export function mount(root) {
// Tutorial
const tutorialTabs = E.tabs(root.querySelector('#tutorial-tabs'), {
activeIndex: 0,
animation: true,
onChange: ({index}) => {
const tabs = ['Overview', 'Features', 'Reviews'];
$(root).find('#tutorial-result').text(`Now viewing: ${tabs[index]}`);
}
});
$(root).find('#tutorial-prev').on('click', () => tutorialTabs.prev());
$(root).find('#tutorial-next').on('click', () => tutorialTabs.next());
}Examples & Variations
Example 1: Standard Tabs
Classic tab design with underline indicator:
Showing all items (24 total)
Showing active items (15 items)
Showing completed items (7 items)
Showing archived items (2 items)
<div id="standard-tabs" class="tabs">
<div class="tab-list" role="tablist">
<button type="button" class="tab-item active" role="tab">All</button>
<button type="button" class="tab-item" role="tab">Active</button>
<button type="button" class="tab-item" role="tab">Completed</button>
<button type="button" class="tab-item" role="tab">Archived</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel"><p>Showing all items (24 total)</p></div>
<div class="tab-panel" role="tabpanel"><p>Showing active items (15 items)</p></div>
<div class="tab-panel" role="tabpanel"><p>Showing completed items (7 items)</p></div>
<div class="tab-panel" role="tabpanel"><p>Showing archived items (2 items)</p></div>
</div>
</div>/**
* Example 1: Standard Tabs - from public/showcase/elements/tabs.
*/
export function mount(root) {
// Examples
E.tabs(root.querySelector('#standard-tabs'));
}Example 2: Pill Tabs
Rounded pill-style tabs for modern interfaces:
Today's stats: 45 visitors, 12 conversions
This week's stats: 312 visitors, 87 conversions
This month's stats: 1,234 visitors, 342 conversions
All time stats: 15,678 visitors, 4,123 conversions
<div id="pill-tabs" class="tabs tabs-pills">
<div class="tab-list" role="tablist">
<button type="button" class="tab-item active" role="tab">Today</button>
<button type="button" class="tab-item" role="tab">This Week</button>
<button type="button" class="tab-item" role="tab">This Month</button>
<button type="button" class="tab-item" role="tab">All Time</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel"><p>Today's stats: 45 visitors, 12 conversions</p></div>
<div class="tab-panel" role="tabpanel"><p>This week's stats: 312 visitors, 87 conversions</p></div>
<div class="tab-panel" role="tabpanel"><p>This month's stats: 1,234 visitors, 342 conversions</p></div>
<div class="tab-panel" role="tabpanel"><p>All time stats: 15,678 visitors, 4,123 conversions</p></div>
</div>
</div>/**
* Example 2: Pill Tabs - from public/showcase/elements/tabs.
*/
export function mount(root) {
E.tabs(root.querySelector('#pill-tabs'));
}Example 3: Vertical Tabs
Side-by-side layout for settings or profiles:
General Settings
Configure general application preferences.
Security Settings
Manage password and two-factor authentication.
Notification Settings
Choose how you receive notifications.
Privacy Settings
Control who can see your information.
<div id="vertical-tabs" class="tabs tabs-vertical">
<div class="tab-list" role="tablist">
<button type="button" class="tab-item active" role="tab">General</button>
<button type="button" class="tab-item" role="tab">Security</button>
<button type="button" class="tab-item" role="tab">Notifications</button>
<button type="button" class="tab-item" role="tab">Privacy</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel">
<h4>General Settings</h4>
<p>Configure general application preferences.</p>
</div>
<div class="tab-panel" role="tabpanel">
<h4>Security Settings</h4>
<p>Manage password and two-factor authentication.</p>
</div>
<div class="tab-panel" role="tabpanel">
<h4>Notification Settings</h4>
<p>Choose how you receive notifications.</p>
</div>
<div class="tab-panel" role="tabpanel">
<h4>Privacy Settings</h4>
<p>Control who can see your information.</p>
</div>
</div>
</div>/**
* Example 3: Vertical Tabs - from public/showcase/elements/tabs.
*/
export function mount(root) {
E.tabs(root.querySelector('#vertical-tabs'));
}Example 4: Tabs with Icons
Enhance tabs with icons for better recognition:
Your documents and files
Database and spreadsheets
Images and videos
<div id="icon-tabs" class="tabs">
<div class="tab-list" role="tablist">
<button type="button" class="tab-item active" role="tab">
<span data-icon="document" class="mr-2"></span>
Documents
</button>
<button type="button" class="tab-item" role="tab">
<span data-icon="database" class="mr-2"></span>
Data
</button>
<button type="button" class="tab-item" role="tab">
<span data-icon="layout" class="mr-2"></span>
Media
</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel"><p>Your documents and files</p></div>
<div class="tab-panel" role="tabpanel"><p>Database and spreadsheets</p></div>
<div class="tab-panel" role="tabpanel"><p>Images and videos</p></div>
</div>
</div>/**
* Example 4: Tabs with Icons - from public/showcase/elements/tabs.
*/
export function mount(root) {
E.tabs(root.querySelector('#icon-tabs'));
}Example 5: Tabs with Badge Counts
Show counts or status with badges:
12 unread messages in your inbox
3 draft messages waiting to be sent
45 messages successfully sent
<div id="badge-tabs" class="tabs">
<div class="tab-list" role="tablist">
<button type="button" class="tab-item active" role="tab">
Inbox <span class="badge badge-primary ml-2">12</span>
</button>
<button type="button" class="tab-item" role="tab">
Drafts <span class="badge badge-secondary ml-2">3</span>
</button>
<button type="button" class="tab-item" role="tab">
Sent <span class="badge badge-success ml-2">45</span>
</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel"><p>12 unread messages in your inbox</p></div>
<div class="tab-panel" role="tabpanel"><p>3 draft messages waiting to be sent</p></div>
<div class="tab-panel" role="tabpanel"><p>45 messages successfully sent</p></div>
</div>
</div>/**
* Example 5: Tabs with Badge Counts - from public/showcase/elements/tabs.
*/
export function mount(root) {
E.tabs(root.querySelector('#badge-tabs'));
}Example 6: Dynamic Content Loading
Load panel content dynamically on tab change:
Content for tab 1, there from the start.
Not loaded yet.
Not loaded yet.
Each tab's content loads the first time you open it.
<div id="dynamic-tabs" class="tabs">
<div class="tab-list" role="tablist">
<button type="button" class="tab-item active" role="tab">Tab 1</button>
<button type="button" class="tab-item" role="tab">Tab 2</button>
<button type="button" class="tab-item" role="tab">Tab 3</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel"><p>Content for tab 1, there from the start.</p></div>
<div class="tab-panel" role="tabpanel"><p class="text-muted">Not loaded yet.</p></div>
<div class="tab-panel" role="tabpanel"><p class="text-muted">Not loaded yet.</p></div>
</div>
</div>
<p class="text-sm mt-2 text-muted">Each tab's content loads the first time you open it.</p>/**
* Dynamic content - a tab's content loads the first time it is opened, with a
* skeleton while it does. Pending loads are cancelled on teardown.
*/
export function mount(root) {
const timers = new Set();
E.tabs(root.querySelector('#dynamic-tabs'), {
onChange: (index, panel) => {
if (panel.dataset.loaded) return;
panel.dataset.loaded = 'loading';
$(panel).html('<div class="skeleton-lines" aria-busy="true"><div class="skeleton skeleton-text"></div><div class="skeleton skeleton-text"></div></div>');
const timer = setTimeout(() => {
timers.delete(timer);
$(panel).html(`<p>Content for tab ${index + 1}, loaded when you opened it.</p>
<p class="text-sm text-muted">Loaded at ${D().format('HH:mm:ss')}</p>`);
panel.dataset.loaded = 'true';
}, 800);
timers.add(timer);
}
});
return () => timers.forEach(clearTimeout);
}Best Practices
Accessibility
- ARIA Roles: Domma automatically adds
role="tablist",role="tab",role="tabpanel" - Keyboard Navigation: Arrow keys to move between tabs, Home/End for first/last, Enter/Space to activate
- Focus Management: Visible focus indicators on tabs, focus moves with arrow keys
- Screen Readers: Proper labelling with
aria-selected,aria-controls,aria-labelledby - Tab Order: Only active tab is in tab order (others reachable via arrow keys)
- Disabled States: Use
aria-disabled="true"for disabled tabs with visual styling
Performance
- Lazy Loading: Load heavy content only when tab is first activated
- Limit Tabs: Keep to 5-7 tabs maximum for usability; use dropdowns or accordions for more
- Content Size: Keep panel content reasonably sized; long content should scroll within panel
- Destroy Unused: Call
destroy()when removing dynamic tab sets - Animation Toggle: Disable animations (
animation: false) for faster switching on low-end devices
Common Gotchas
| Issue | Solution |
|---|---|
| Wrong panel shows for tab | Ensure tab items and panels are in the same order. First tab item corresponds to first panel, etc. |
| Content overflows tabs | Set max-width on tab list or make tabs scrollable horizontally on mobile: `.tab-list { |
overflow-x: auto; }` |
| Active tab not visible | When programmatically switching tabs, scroll the tab list to show active tab if it's off-screen. |
| Animation jank | Ensure panels have consistent heights or set explicit height on .tab-content to prevent layout shift. |
| Nested tabs conflict | Give each tab set a unique ID and initialise separately. Nested tabs work but can confuse users. |
Tips & Tricks
- Deep Linking: Use URL hash to pre-select tab:
if (location.hash === '#features') tabs.show(1); - Persistent State: Save active tab to localStorage and restore on page load
- Validation: In forms, validate tab content before allowing switch to next tab
- Mobile Dropdown: On small screens, convert tabs to dropdown for better UX
- Smooth Transitions: Use
animation: truefor professional feel; crossfade looks better than slide - Equal Width Tabs: Use flexbox for equal-width tabs:
.tab-list { display: flex; } .tab-item { flex: 1; }
API Reference
Constructor Options
| Option | Type | Default | Description |
|---|---|---|---|
activeIndex |
Number | 0 |
Index of initially active tab (0-based) |
animation |
Boolean | true |
Enable smooth fade transitions between panels |
onChange |
Function | null |
Callback fired when active tab changes: (index, panel) => {} |
Methods
| Method | Parameters | Returns | Description |
|---|---|---|---|
show(index) |
index: Number |
this | Activates the tab at specified index (0-based) |
getActive() |
None | Number | Returns the index of currently active tab |
next() |
None | this | Activates the next tab (wraps to first if at end) |
prev() |
None | this | Activates the previous tab (wraps to last if at start) |
destroy() |
None | void | Removes event listeners and cleans up instance |
Events
| Event | Parameters | Description |
|---|---|---|
onChange |
index: Number, panel: Element |
Fired when user or program switches to a different tab |
CSS Classes
| Class | Description |
|---|---|
.tabs |
Main container for tab component |
.tabs-pills |
Modifier for pill-style tabs with rounded backgrounds |
.tabs-vertical |
Modifier for vertical (side-by-side) layout |
.tab-list |
Container for tab headers (ul element) |
.tab-item |
Individual tab header (li element) |
.tab-item.active |
Currently active tab header |
.tab-content |
Container for all tab panels |
.tab-panel |
Individual content panel (div element) |
.tab-panel.active |
Currently visible content panel |
Config Engine Integration
Declaratively initialise tabs using Domma's Config Engine.
Basic Setup
$.setup({
'#product-tabs': {
component: 'tabs',
options: {
activeIndex: 0,
animation: true
}
}
});
With Change Tracking
$.setup({
'#analytics-tabs': {
component: 'tabs',
options: {
activeIndex: 0,
animation: true,
onChange: (index) => {
const views = ['Today', 'Week', 'Month', 'Year'];
console.log(`Viewing ${views[index]} analytics`);
// Could fetch data for selected period
}
}
}
});
Multiple Tab Sets
$.setup({
'.product-tabs': {
component: 'tabs',
options: {
activeIndex: 0,
animation: true
}
},
'#settings-tabs': {
component: 'tabs',
options: {
activeIndex: 1, // Start on second tab
animation: false // No animation
}
}
});
Demo
Initialised with Config Engine
Smooth animations enabled
onChange callback fires
<div class="config-demo-tabs tabs">
<div class="tab-list" role="tablist">
<button type="button" class="tab-item active" role="tab">Config Tab 1</button>
<button type="button" class="tab-item" role="tab">Config Tab 2</button>
<button type="button" class="tab-item" role="tab">Config Tab 3</button>
</div>
<div class="tab-content">
<div class="tab-panel active" role="tabpanel"><p>Initialised with Config Engine</p></div>
<div class="tab-panel" role="tabpanel"><p>Smooth animations enabled</p></div>
<div class="tab-panel" role="tabpanel"><p>onChange callback fires</p></div>
</div>
</div>/**
* Demo - from public/showcase/elements/tabs.
*/
export function mount(root) {
// Config Engine demo
$.setup({
'.config-demo-tabs': {
component: 'tabs',
options: {
activeIndex: 0,
animation: true,
onChange: (index) => {
console.log('Config demo tab changed to:', index);
}
}
}
});
}CSS Customisation
Override these CSS variables to customise Tabs appearance and match your design system.
| Variable | Default | Controls |
|---|---|---|
--dm-tab-border |
var(--dm-border) |
Tab border colour |
--dm-tab-hover-bg |
var(--dm-hover-bg) |
Tab hover background |
--dm-tab-active-border |
var(--dm-primary) |
Active tab bottom border colour |
--dm-tab-active-text |
var(--dm-primary) |
Active tab text colour |
--dm-primary |
var(--dm-blue-600) |
Primary brand colour |
Example Override
:root {
/* Custom active tab colour */
--dm-tab-active-border: #16a34a; /* Green */
--dm-tab-active-text: #16a34a;
/* Subtle hover */
--dm-tab-hover-bg: rgba(0,0,0,0.02);
}
Full CSS Customisation Cheat-Sheet →
Related Elements
Tabs work great with these complementary components:
Accordion
Alternative to tabs when vertical space isn't limited; all content on one page.
Card
Wrap tab panels in cards for better visual separation and structure.
Modal
Use tabs within modals for complex forms or multi-section content.
Loader
Show loading states while tab content loads asynchronously.
List Group
Use as a vertical tab-style navigation when a sidebar layout suits better than horizontal tabs.