Documentation

Documentation - Tabs.

Discover Marssel: an intelligent, minimal configuration CSS framework designed for fast interfaces and a simplified developer experience.

Tabs

The Marssel tab system offers a complete, accessible and powerful solution for creating tabbed interfaces without manual JavaScript.

Configuration de base

The TabsManager is automatically initialized:

const marssel = new Marssel();
// The TabsManager is ready to use

HTML structure

Preview

Content 1

First tab.

Content 2

Second tab.

Content 3

Third tab.

<div class="tabs-container" data-tabs="mon-groupe">
    <!-- 1. Hidden radio inputs -->
    <input type="radio" id="tab1" name="mes-tabs" class="tab-radio" checked>
    <input type="radio" id="tab2" name="mes-tabs" class="tab-radio">
    <input type="radio" id="tab3" name="mes-tabs" class="tab-radio">

    <!-- 2. Tab navigation -->
    <div class="tabs-nav---[d-[flex]+mb-[20px]+border-b-[1px_solid_e0e0e0]]">
        <label for="tab1"
            class="tab-label---[p-[15px]+cursor-[pointer]+c-[666]+border-b-[3px_solid_transparent]+transition-[all_0.3s]]">
            Tab 1
        </label>
        <label for="tab2" class="tab-label">
            Tab 2
        </label>
        <label for="tab3" class="tab-label">
            Tab 3
        </label>
    </div>

    <!-- 3. Content of the panels -->
    <div class="tabs-content">
        <div class="tab-panel---[p-[20px]+border-[1px_solid_e0e0e0]+rounded-[5px]]">
            <h4>Content 1</h4>
            <p>First tab.</p>
        </div>
        <div class="tab-panel">
            <h4>Content 2</h4>
            <p>Second tab.</p>
        </div>
        <div class="tab-panel">
            <h4>Content 3</h4>
            <p>Third tab.</p>
        </div>
    </div>
</div>

Important rules

✅ To do :
- Each radio input must have a unique ID
- All radios in a group must have the same name attribute
- Labels must have a for attribute corresponding to the radio ID
- The order of the panels must correspond to the order of the radios
- Add checked to the first radio to display it by default

Configuration

Available data attributes:

Attribute                       Kind                                     Default       Description
----------------------------------------------------------------------------------------------------------------------
data-tabs                       string                                  self-generated  Unique group identifier
data-tabs-orientation           horizontal | vertical                   horizontal   Tab orientation
data-tabs-preset                default | pills | minimal | bordered    default      Predefined style
data-tabs-animated              true | false                            true         Activate animations
data-tabs-responsive            true | false                            true         Responsive mode
data-tabs-mobile-breakpoint     string                                  768px        Moving breakpoint
data-tabs-active-color          color                                   #0066cc      Color of active tab
data-tabs-active-border-width   string                                  3px          Active border thickness
data-tabs-keyboard              true | false                            true         Enables keyboard navigation

Example with full configuration

<div class="tabs-container"
    data-tabs="advanced-tabs"
    data-tabs-orientation="vertical"
    data-tabs-preset="pills"
    data-tabs-animated="true"
    data-tabs-responsive="true"
    data-tabs-mobile-breakpoint="768px"
    data-tabs-active-color="#10b981 | red | var(--theme-primary)"
    data-tabs-active-border-width="4px"
    data-tabs-keyboard="true"
    id="my-tabs">
    <!-- Tab structure -->
</div>

Style presets: Default

Standard style with lower border having the following characteristics:

  • Bottom border 3px
  • Blue color (#0066cc)
  • Transparent background
  • 15px padding

<div class="tabs-container" data-tabs-preset="default">
    <!-- Bottom border on active tab -->
</div>

Style Presets: Pills.

Tabs in the shape of rounded pills with the following characteristics:

  • Border-radius: 20px
  • Light gray background (#f0f0f0)
  • No border
  • Padding 10px 20px

<div class="tabs-container" data-tabs-preset="pills">
    <!-- Rounded tabs with bottom -->
</div>

Style Presets: Minimal

Clean minimalist style with the following characteristics:

  • 2px thin border
  • Neutral color (#333)
  • Transparent background
  • Padding 10px 15px

<div class="tabs-container" data-tabs-preset="bordered">
    <!-- Borders on all sides -->
</div>

Style Presets: Bordered

Tabs with full borders with the following characteristics:

  • Full border 1px
  • White background
  • Active border 2px
  • Padding 12px 20px

<div class="tabs-container"
    data-tabs-preset="pills"
    data-tabs-active-color="#10b981">
    <!-- Emerald Green Pills -->
</div>

JavaScript API: Navigation methods.

Activate a specific tab by its index (0-based) with activateTab(groupId, index) :

// Activate the 2nd tab
marssel.tabsManager.activateTab('mon-groupe', 1);

/*Settings:

groupId (string) : Tab group identifier
index (number) : Tab index (starts at 0)

Return: boolean - true if successful, false otherwise */

Activate the next tab (loop at the beginning if at the end) with nextTab(groupId) :

marssel.tabsManager.nextTab('mon-groupe');

//Return: boolean

Activate the previous tab (loop at the end if at the beginning) with previousTab(groupId) :

marssel.tabsManager.previousTab('mon-groupe');

//Return: boolean

JavaScript API: Information Methods

Retrieves the object of the currently active tab with getActiveTab(groupId) :

const activeTab = marssel.tabsManager.getActiveTab('mon-groupe');
console.log(activeTab);
// {
//   radio: HTMLInputElement,
//   label: HTMLLabelElement,
//   panel: HTMLDivElement,
//   index: 0,
//   id: 'tab1'
// }

// Return: Object | null

Get the index of the active tab with getActiveIndex(groupId) :

const index = marssel.tabsManager.getActiveIndex('mon-groupe');
console.log(index); // 0, 1, 2, etc.

//Return: number - Index of the active tab, or -1 if none

Retrieves the list of all tab groups initialized with getAllGroups() :

const groups = marssel.tabsManager.getAllGroups();
console.log(groups); // ['groupe-1', 'groupe-2', ...]

//Return: string[]

Get the full object of a tab group with getGroup(groupId) :

const group = marssel.tabsManager.getGroup('mon-groupe');
console.log(group);
// {
//   id: 'mon-groupe',
//   container: HTMLDivElement,
//   tabs: [...],
//   config: {...},
//   groupName: 'mes-tabs',
//   initialized: true
// }

// Return: Object | null

JavaScript API: Control Methods

Disables a tab (makes it unclickable) with disableTab(groupId, index) :

marssel.tabsManager.disableTab('mon-groupe', 2);

//Effect :

//Input radio disabled = true
//Label opacity: 0.5 et pointer-events: none

//Return: Boolean

Reactivates a previously disabled tab with enableTab(groupId, index) :

marssel.tabsManager.enableTab('mon-groupe', 2);

//Return: boolean

Destroys a group of tabs and cleans up all resources with destroy(groupId) :

marssel.tabsManager.destroy('mon-groupe');

//Actions performed:

//Deleting event listeners
//Cleaning up CSS styles
//Deleting the group from the Map

//Return: boolean

Completely cleans the TabsManager (all groups) with cleanup() :

marssel.tabsManager.cleanup();

// Warning: Destroys all groups and disconnects the observer.

JavaScript API: Events

Issued when switching tabs with marssel:tab:change :

document.addEventListener('marssel:tab:change', (event) => {
    const {
        groupId,         // Group ID
        activeIndex,     // Tab index enabled
        activeTab,       // Active radio input
        activeLabel,     // Active label
        activePanel,     // Active panel
        previousIndex    // Previous index
    } = event.detail;

    console.log(`Change in ${groupId}: ${previousIndex} → ${activeIndex}`);
});

event.detail properties:

| Property       | Kind             | Description                                 |
|-----------------|--------------------|---------------------------------------------|
| groupId         | string             | Group ID                       |
| activeIndex     | number             | Tab index enabled (0-based)          |
| activeTab       | HTMLInputElement   | Active radio <input> element                 |
| activeLabel     | HTMLLabelElement   | Active tab label                     |
| activePanel     | HTMLDivElement     | Active content panel                      |
| previousIndex   | number             | Previous tab index                 |

Lazy loading of content:

document.addEventListener('marssel:tab:change', async (e) => {
    const { activePanel, groupId } = e.detail;

    if (groupId === 'lazy-tabs' && activePanel.dataset.loaded !== 'true') {
        const content = await fetchContent(activePanel.dataset.url);
        activePanel.innerHTML = content;
        activePanel.dataset.loaded = 'true';
    }
});

Analytics:

document.addEventListener('marssel:tab:change', (e) => {
    gtag('event', 'tab_change', {
        group_id: e.detail.groupId,
        tab_index: e.detail.activeIndex
    });
});

Synchronization between groups:

document.addEventListener('marssel:tab:change', (e) => {
    if (e.detail.groupId === 'group-1') {
        marssel.tabsManager.activateTab('group-2', e.detail.activeIndex);
    }
});

JavaScript API: Accessibility

The TabsManager automatically manages keyboard navigation:

| Touch         | Action                          |
|----------------|--------------------------------|
| → / ↓          | Next tab                 |
| ← / ↑          | Previous tab               |
| Home           | First tab                 |
| End            | Last tab                 |
| Enter / Space  | Enable focused tab        |
| Tab            | Exit tab group       |

The TabsManager automatically updates ARIA attributes:

<!-- Active tab label -->
<label for="tab1" aria-selected="true" tabindex="0">
    Tab 1
</label>

<!-- Active panel -->
<div class="tab-panel" aria-hidden="false">
    Visible content
</div>

<!-- Inactive panel -->
<div class="tab-panel" aria-hidden="true" style="display: none;">
    Hidden content
</div>

Responsive Design

With data-tabs-responsive='true', tabs automatically switch to vertical mode on mobile:

<div class="tabs-container"
        data-tabs-responsive="true"
        data-tabs-mobile-breakpoint="768px">
    <!-- On &lt; 768px: vertical -->
    <!-- On &gt;= 768px: horizontal -->
</div>

Mobile behavior on Desktop (>= 768px):

.tabs-nav {
    flex-direction: row;
    border-bottom: 1px solid #e0e0e0;
}

.tab-label {
    border-bottom: 3px solid transparent;
}

#tab1:checked ~ .tabs-nav label[for="tab1"] {
    border-bottom: 3px solid #0066cc;
}

Mobile behavior on Mobile (< 768px):

.tabs-nav {
    flex-direction: column;
}

.tab-label {
    border-left: 3px solid transparent;
    border-bottom: 1px solid #e0e0e0;
}

#tab1:checked ~ .tabs-nav label[for="tab1"] {
    border-left: 3px solid #0066cc;
    border-bottom: 1px solid #e0e0e0;
}

Responsive customization:

<!-- Responsive navigation with Marsl classes -->
<div class="tabs-nav---[d-[flex]+md--flex-direction-[column]+gap-[10px]]">
    <!-- On tablet/mobile: column -->
    <!-- On desktop: line -->
</div>

Advanced Examples

Tabs with icons:

Preview
Home content
Content settings
<div class="tabs-container" data-tabs="icon-tabs">
    <input type="radio" id="icon1" name="icons" class="tab-radio" checked>
    <input type="radio" id="icon2" name="icons" class="tab-radio">

    <div class="tabs-nav---[d-[flex]+mb-[20px]]">
        <label for="icon1"
                class="tab-label---[d-[flex]+flex-direction-[column]+items-[center]+gap-[8px]+p-[15px]]">
            <span class="fs-[24px]">🏠</span>
            <span>Welcome</span>
        </label>
        <label for="icon2" class="tab-label">
            <span class="fs-[24px]">⚙️</span>
            <span>Settings</span>
        </label>
    </div>

    <div class="tabs-content">
        <div class="tab-panel---[p-[20px]]">Home content</div>
        <div class="tab-panel">Content settings</div>
    </div>
</div>

Tabs with badges

Preview
5 new posts
2 notifications
<div class="tabs-container" data-tabs="badge-tabs">
    <input type="radio" id="msg" name="badges" class="tab-radio" checked>
    <input type="radio" id="notif" name="badges" class="tab-radio">

    <div class="tabs-nav---[d-[flex]+mb-[20px]]">
        <label for="msg"
                class="tab-label---[flex-[1]+p-[15px]+pos-[relative]+text-align-[center]]">
            Messages
            <span class="badge---[pos-[absolute]+top-[8px]+right-[8px]+bg-[D41212]+c-[white]+w-[20px]+h-[20px]+rounded-[50%]+fs-[12px]+d-[flex]+items-[center]+justify-[center]]">
                5
            </span>
        </label>
        <label for="notif" class="tab-label">
            Notifications
            <span class="badge---[pos-[absolute]+top-[8px]+right-[8px]+bg-[0B63F3]+c-[white]+w-[20px]+h-[20px]+rounded-[50%]+fs-[12px]+d-[flex]+items-[center]+justify-[center]]">
                2
            </span>
        </label>
    </div>

    <div class="tabs-content">
        <div class="tab-panel---[p-[20px]]">5 new posts</div>
        <div class="tab-panel">2 notifications</div>
    </div>
</div>

Vertical tabs

Preview
General configuration
Security Settings
<div class="tabs-container"
        data-tabs="vertical-tabs"
        data-tabs-orientation="vertical"
        id="vertical-container"
        style="min-height: 300px;">

    <input type="radio" id="v1" name="vertical" class="tab-radio" checked>
    <input type="radio" id="v2" name="vertical" class="tab-radio">

    <div class="tabs-nav---[w-[200px]+bg-[f9f9f9]]">
        <label for="v1" class="tab-label---[p-[15px]+d-[block]]">
            Configuration
        </label>
        <label for="v2" class="tab-label">
            Security
        </label>
    </div>

    <div class="tabs-content---[flex-[1]+p-[20px]]">
        <div class="tab-panel">General configuration</div>
        <div class="tab-panel">Security Settings</div>
    </div>
</div>

Nested tabs

Preview

Section 1

Sub A Content
Sub B Content
Section 2
<div class="tabs-container" data-tabs="parent-tabs">
    <input type="radio" id="p1" name="parent" class="tab-radio" checked>
    <input type="radio" id="p2" name="parent" class="tab-radio">

    <div class="tabs-nav---[d-[flex]+mb-[20px]]">
        <label for="p1" class="tab-label">Section 1</label>
        <label for="p2" class="tab-label">Section 2</label>
    </div>

    <div class="tabs-content">
        <div class="tab-panel---[p-[20px]]">
            <h4 class="mb-[20px]">Section 1</h4>
            <div class="tabs-container" data-tabs="nested-tabs">
                <input type="radio" id="n1" name="nested" class="tab-radio" checked>
                <input type="radio" id="n2" name="nested" class="tab-radio">

                <div class="tabs-nav---[d-[flex]+mb-[15px]]">
                    <label for="n1" class="tab-label---[p-[10px]+fs-[14px]]">Sub A</label>
                    <label for="n2" class="tab-label---[p-[10px]+fs-[14px]]">Sub B</label>
                </div>

                <div class="tabs-content">
                    <div class="tab-panel---[p-[15px]]">Sub A Content</div>
                    <div class="tab-panel">Sub B Content</div>
                </div>
            </div>
        </div>

        <div class="tab-panel">Section 2</div>
    </div>
</div>

Lazy loading:

Preview
Pre-loaded content
Loading...
<div class="tabs-container" data-tabs="lazy-tabs">
    <input type="radio" id="l1" name="lazy" class="tab-radio" checked>
    <input type="radio" id="l2" name="lazy" class="tab-radio">

    <div class="tabs-nav---[d-[flex]+mb-[20px]]">
        <label for="l1" class="tab-label">Charge</label>
        <label for="l2" class="tab-label">Lazy</label>
    </div>

    <div class="tabs-content">
        <div class="tab-panel---[p-[20px]]" data-loaded="true">
            Pre-loaded content
        </div>
        <div class="tab-panel---[p-[20px]]" data-lazy-url="/api/content">
            <div class="text-align-[center]">Loading...</div>
        </div>
    </div>
</div>

<script>
document.addEventListener('marssel:tab:change', async (e) => {
    const panel = e.detail.activePanel;

    if (panel && panel.dataset.lazyUrl && !panel.dataset.loaded) {
        const response = await fetch(panel.dataset.lazyUrl);
        const content = await response.text();
        panel.innerHTML = content;
        panel.dataset.loaded = 'true';
    }
});
</script>

Programmatic control:

Preview
Content of the first tab.
Contents of the second tab.
Contents of the third tab.
<div class="mb-[20px]">
    <button onclick="marssel.tabsManager.previousTab('control-tabs')"
            class="px-[15px] py-[8px] bg-[0B63F3] c-[white] rounded-[5px]">
        ← Previous
    </button>
    <button onclick="marssel.tabsManager.nextTab('control-tabs')"
            class="px-[15px] py-[8px] bg-[0B63F3] c-[white] rounded-[5px]">
        Next →
    </button>
    <button onclick="marssel.tabsManager.disableTab('control-tabs', 2)"
            class="px-[15px] py-[8px] bg-[D41212] c-[white] rounded-[5px]">
        Disable Tab 3
    </button>
</div>

<div class="tabs-container" data-tabs="control-tabs">
    <input type="radio" id="c1" name="control" class="tab-radio" checked>
    <input type="radio" id="c2" name="control" class="tab-radio">
    <input type="radio" id="c3" name="control" class="tab-radio">

    <div class="tabs-nav---[d-[flex]+mb-[20px]]">
        <label for="c1" class="tab-label">Tab 1</label>
        <label for="c2" class="tab-label">Tab 2</label>
        <label for="c3" class="tab-label">Tab 3 (Can be deactivated)</label>
    </div>

    <div class="tabs-content">
        <div class="tab-panel---[p-[20px]]">
            Content of the first tab.
        </div>
        <div class="tab-panel">
            Contents of the second tab.
        </div>
        <div class="tab-panel">
            Contents of the third tab.
        </div>
    </div>
</div>