Documentation - Tabs.
Discover Marssel: an intelligent, minimal configuration CSS framework designed for fast interfaces and a simplified developer experience.
Startup
Basic concepts
Utilities
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
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 < 768px: vertical -->
<!-- On >= 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:
<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
<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
<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
Section 1
<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:
<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:
<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>