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');
Simple 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

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:

Example 1: Standard Tabs

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:

Example 2: Pill Tabs

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:

Example 3: Vertical Tabs

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:

Example 4: Tabs with Icons

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:

Example 5: Tabs with Badge Counts

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:

Example 6: Dynamic Content Loading

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: true for 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

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.