Content Tabs Configuration¶
This page provides comprehensive configuration options for content tabs in MkDocs Material, including advanced features and customization options.
Basic Configuration¶
Enable Content Tabs¶
Add to your mkdocs.yml
:
markdown_extensions:
- pymdownx.tabbed:
alternate_style: true
slugify: !!python/object/apply:pymdownx.slugs.slugify
kwds:
case: lower
theme:
features:
- content.tabs.link # Enable linked tabs
Configuration Options¶
Option | Type | Default | Description |
---|---|---|---|
alternate_style |
boolean | false |
Use Material Design tabs style |
slugify |
function | None |
Function to generate tab IDs |
combine_header_slug |
boolean | false |
Combine with header slugs |
separator |
string | "-" |
Separator for generated slugs |
Advanced Configuration¶
Custom Slugification¶
markdown_extensions:
- pymdownx.tabbed:
alternate_style: true
slugify: !!python/object/apply:pymdownx.slugs.slugify
kwds:
case: lower # Convert to lowercase
normalize: nfc # Unicode normalization
ascii_only: false # Allow non-ASCII characters
combine_header_slug: true # Include header in tab ID
separator: "_" # Use underscore separator
Tab Linking Configuration¶
theme:
features:
- content.tabs.link # Synchronize tabs with same labels
- content.code.copy # Copy button for code in tabs
- content.code.annotate # Code annotations in tabs
- navigation.instant # Instant navigation preserves tab state
Theme Customization¶
CSS Variables¶
/* docs/assets/stylesheets/tabs-custom.css */
:root {
/* Tab container */
--md-tabs-background: var(--md-default-bg-color);
--md-tabs-border: var(--md-default-fg-color--lightest);
--md-tabs-border-radius: 0.2rem;
--md-tabs-spacing: 1rem;
/* Tab labels */
--md-tabs-label-color: var(--md-default-fg-color--light);
--md-tabs-label-background: transparent;
--md-tabs-label-padding: 0.75rem 1rem;
--md-tabs-label-font-weight: 500;
/* Active tab */
--md-tabs-active-color: var(--md-accent-fg-color);
--md-tabs-active-background: var(--md-accent-fg-color--transparent);
--md-tabs-active-border: var(--md-accent-fg-color);
/* Hover effects */
--md-tabs-hover-color: var(--md-accent-fg-color);
--md-tabs-hover-background: var(--md-accent-fg-color--transparent);
/* Content area */
--md-tabs-content-padding: 1.5rem 0;
--md-tabs-content-background: transparent;
}
/* Dark mode adjustments */
[data-md-color-scheme="slate"] {
--md-tabs-background: var(--md-code-bg-color);
--md-tabs-border: var(--md-default-fg-color--lightest);
}
Advanced Styling¶
/* Custom tab styles */
.md-typeset .tabbed-set {
position: relative;
margin: var(--md-tabs-spacing) 0;
border: 1px solid var(--md-tabs-border);
border-radius: var(--md-tabs-border-radius);
background: var(--md-tabs-background);
overflow: hidden;
}
/* Tab header container */
.md-typeset .tabbed-labels {
display: flex;
background: var(--md-tabs-background);
border-bottom: 1px solid var(--md-tabs-border);
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.md-typeset .tabbed-labels::-webkit-scrollbar {
display: none;
}
/* Individual tab labels */
.md-typeset .tabbed-labels > label {
flex-shrink: 0;
padding: var(--md-tabs-label-padding);
color: var(--md-tabs-label-color);
background: var(--md-tabs-label-background);
font-weight: var(--md-tabs-label-font-weight);
cursor: pointer;
transition: all 0.2s ease-in-out;
border-bottom: 2px solid transparent;
position: relative;
}
/* Hover effects */
.md-typeset .tabbed-labels > label:hover {
color: var(--md-tabs-hover-color);
background: var(--md-tabs-hover-background);
}
/* Active tab styling */
.md-typeset .tabbed-labels > label[for]:checked {
color: var(--md-tabs-active-color);
background: var(--md-tabs-active-background);
border-bottom-color: var(--md-tabs-active-border);
}
/* Focus styles for accessibility */
.md-typeset .tabbed-labels > label:focus-visible {
outline: 2px solid var(--md-accent-fg-color);
outline-offset: -2px;
}
/* Tab content */
.md-typeset .tabbed-content {
padding: var(--md-tabs-content-padding);
background: var(--md-tabs-content-background);
min-height: 2rem;
}
/* Animations */
.md-typeset .tabbed-content > .tabbed-block {
animation: tabFadeIn 0.3s ease-in-out;
}
@keyframes tabFadeIn {
from {
opacity: 0;
transform: translateY(0.5rem);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Icon support in tab labels */
.md-typeset .tabbed-labels > label .twemoji {
margin-right: 0.5rem;
vertical-align: text-bottom;
}
/* Badge support in tab labels */
.md-typeset .tabbed-labels > label .md-badge {
margin-left: 0.5rem;
font-size: 0.7em;
vertical-align: middle;
}
JavaScript Enhancements¶
Tab State Management¶
// docs/assets/javascripts/tabs-enhanced.js
class TabManager {
constructor() {
this.initializeTabGroups();
this.setupKeyboardNavigation();
this.setupTabMemory();
}
initializeTabGroups() {
// Find all tabbed sets with linking enabled
const tabSets = document.querySelectorAll('.tabbed-set');
tabSets.forEach(tabSet => {
const labels = tabSet.querySelectorAll('.tabbed-labels > label');
labels.forEach(label => {
label.addEventListener('click', (e) => {
this.handleTabClick(e.target);
});
});
});
}
handleTabClick(clickedLabel) {
const tabText = clickedLabel.textContent.trim();
// Save tab preference
this.saveTabPreference(tabText);
// Sync linked tabs
this.syncLinkedTabs(tabText);
// Trigger custom event
this.dispatchTabChangeEvent(tabText);
}
syncLinkedTabs(activeTabText) {
// Find all tabs with the same text content
const allLabels = document.querySelectorAll('.tabbed-labels > label');
allLabels.forEach(label => {
if (label.textContent.trim() === activeTabText) {
label.click();
}
});
}
saveTabPreference(tabText) {
// Save to localStorage for persistence
const preferences = this.getTabPreferences();
preferences[tabText] = Date.now();
localStorage.setItem('tabPreferences', JSON.stringify(preferences));
}
getTabPreferences() {
try {
return JSON.parse(localStorage.getItem('tabPreferences')) || {};
} catch {
return {};
}
}
restoreTabPreferences() {
const preferences = this.getTabPreferences();
const sortedTabs = Object.entries(preferences)
.sort(([,a], [,b]) => b - a);
// Restore most recently used tabs
sortedTabs.slice(0, 5).forEach(([tabText]) => {
const label = document.querySelector(
`.tabbed-labels > label[data-tab-text="${tabText}"]`
);
if (label) {
label.click();
}
});
}
setupKeyboardNavigation() {
document.addEventListener('keydown', (e) => {
const activeElement = document.activeElement;
if (activeElement.matches('.tabbed-labels > label')) {
this.handleKeyNavigation(e, activeElement);
}
});
}
handleKeyNavigation(e, currentLabel) {
const labels = Array.from(
currentLabel.parentElement.querySelectorAll('label')
);
const currentIndex = labels.indexOf(currentLabel);
let newIndex = currentIndex;
switch (e.key) {
case 'ArrowLeft':
newIndex = Math.max(0, currentIndex - 1);
break;
case 'ArrowRight':
newIndex = Math.min(labels.length - 1, currentIndex + 1);
break;
case 'Home':
newIndex = 0;
break;
case 'End':
newIndex = labels.length - 1;
break;
default:
return;
}
if (newIndex !== currentIndex) {
e.preventDefault();
labels[newIndex].focus();
labels[newIndex].click();
}
}
setupTabMemory() {
// Restore preferences on page load
document.addEventListener('DOMContentLoaded', () => {
this.restoreTabPreferences();
});
// Handle instant navigation
document.addEventListener('DOMContentLoaded', () => {
if (window.location.instant) {
this.restoreTabPreferences();
}
});
}
dispatchTabChangeEvent(tabText) {
const event = new CustomEvent('tabChanged', {
detail: { tabText, timestamp: Date.now() }
});
document.dispatchEvent(event);
}
}
// Initialize tab manager
if (typeof window !== 'undefined') {
window.tabManager = new TabManager();
}
// Analytics integration
document.addEventListener('tabChanged', (e) => {
// Track tab usage for analytics
if (window.gtag) {
gtag('event', 'tab_changed', {
'custom_parameter': e.detail.tabText
});
}
});
Advanced Features¶
Custom Tab Types¶
# mkdocs.yml - Custom fence types for special tabs
markdown_extensions:
- pymdownx.superfences:
custom_fences:
- name: tabs-code
class: tabs-code
format: !!python/name:pymdownx.superfences.fence_code_format
- name: tabs-compare
class: tabs-compare
format: !!python/name:pymdownx.superfences.fence_code_format
Tab Groups with IDs¶
<!-- Synchronized tab groups -->
=== "Option A" id="sync-group-1"
Content for Option A
=== "Option B" id="sync-group-1"
Content for Option B
<!-- Second group with same IDs -->
=== "Option A" id="sync-group-1"
More content for Option A
=== "Option B" id="sync-group-1"
More content for Option B
Conditional Tab Content¶
{% if config.extra.version >= "2.0" %}
=== "Version 2.0+"
```python
# New API in version 2.0+
from mylib import new_feature
result = new_feature.process()
```
{% endif %}
=== "Legacy Version"
```python
# Compatible with all versions
from mylib import legacy_api
result = legacy_api.process()
```
Responsive Design¶
Mobile Optimization¶
/* Mobile-specific tab styles */
@media screen and (max-width: 768px) {
.md-typeset .tabbed-labels {
/* Enable horizontal scrolling on mobile */
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scroll-behavior: smooth;
}
.md-typeset .tabbed-labels > label {
/* Ensure touch targets are adequate */
min-width: 44px;
min-height: 44px;
padding: 0.75rem 1rem;
white-space: nowrap;
}
/* Add scroll indicators */
.md-typeset .tabbed-labels::before,
.md-typeset .tabbed-labels::after {
content: "";
position: absolute;
top: 0;
bottom: 0;
width: 1rem;
pointer-events: none;
z-index: 1;
}
.md-typeset .tabbed-labels::before {
left: 0;
background: linear-gradient(
to right,
var(--md-default-bg-color),
transparent
);
}
.md-typeset .tabbed-labels::after {
right: 0;
background: linear-gradient(
to left,
var(--md-default-bg-color),
transparent
);
}
}
/* Tablet optimization */
@media screen and (min-width: 769px) and (max-width: 1024px) {
.md-typeset .tabbed-labels > label {
padding: 0.75rem 1.25rem;
}
}
Touch Gestures¶
// Touch gesture support for mobile
class TouchTabNavigation {
constructor(tabSet) {
this.tabSet = tabSet;
this.setupTouchEvents();
}
setupTouchEvents() {
let startX = 0;
let startY = 0;
let isScrolling = false;
this.tabSet.addEventListener('touchstart', (e) => {
startX = e.touches[0].clientX;
startY = e.touches[0].clientY;
isScrolling = false;
});
this.tabSet.addEventListener('touchmove', (e) => {
const deltaX = Math.abs(e.touches[0].clientX - startX);
const deltaY = Math.abs(e.touches[0].clientY - startY);
if (deltaY > deltaX) {
isScrolling = true;
}
});
this.tabSet.addEventListener('touchend', (e) => {
if (isScrolling) return;
const deltaX = e.changedTouches[0].clientX - startX;
if (Math.abs(deltaX) > 50) {
this.handleSwipe(deltaX > 0 ? 'right' : 'left');
}
});
}
handleSwipe(direction) {
const activeLabel = this.tabSet.querySelector(
'.tabbed-labels > label:checked'
);
const labels = Array.from(
this.tabSet.querySelectorAll('.tabbed-labels > label')
);
const currentIndex = labels.indexOf(activeLabel);
let newIndex = currentIndex;
if (direction === 'left' && currentIndex < labels.length - 1) {
newIndex = currentIndex + 1;
} else if (direction === 'right' && currentIndex > 0) {
newIndex = currentIndex - 1;
}
if (newIndex !== currentIndex) {
labels[newIndex].click();
}
}
}
// Initialize touch navigation for all tab sets
document.addEventListener('DOMContentLoaded', () => {
const tabSets = document.querySelectorAll('.tabbed-set');
tabSets.forEach(tabSet => {
new TouchTabNavigation(tabSet);
});
});
Performance Optimization¶
Lazy Loading¶
// Lazy load tab content
class LazyTabContent {
constructor() {
this.setupIntersectionObserver();
this.setupTabContentLoading();
}
setupIntersectionObserver() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
this.loadTabContent(entry.target);
}
});
}, { threshold: 0.1 });
}
setupTabContentLoading() {
document.addEventListener('click', (e) => {
if (e.target.matches('.tabbed-labels > label')) {
const targetId = e.target.getAttribute('for');
const content = document.getElementById(targetId);
if (content && content.hasAttribute('data-lazy')) {
this.loadTabContent(content);
}
}
});
}
loadTabContent(contentElement) {
const src = contentElement.getAttribute('data-src');
if (src && !contentElement.hasAttribute('data-loaded')) {
fetch(src)
.then(response => response.text())
.then(html => {
contentElement.innerHTML = html;
contentElement.setAttribute('data-loaded', 'true');
contentElement.removeAttribute('data-lazy');
})
.catch(error => {
console.error('Failed to load tab content:', error);
contentElement.innerHTML = '<p>Failed to load content.</p>';
});
}
}
}
// Initialize lazy loading
new LazyTabContent();
Accessibility Features¶
ARIA Implementation¶
/* Enhanced accessibility styles */
.md-typeset .tabbed-labels {
role: tablist;
}
.md-typeset .tabbed-labels > label {
role: tab;
aria-selected: false;
tabindex: -1;
}
.md-typeset .tabbed-labels > label:checked {
aria-selected: true;
tabindex: 0;
}
.md-typeset .tabbed-content {
role: tabpanel;
}
/* High contrast mode support */
@media (prefers-contrast: high) {
.md-typeset .tabbed-labels > label {
border: 2px solid;
}
.md-typeset .tabbed-labels > label:checked {
background: CanvasText;
color: Canvas;
}
}
/* Reduced motion support */
@media (prefers-reduced-motion: reduce) {
.md-typeset .tabbed-content > .tabbed-block {
animation: none;
}
.md-typeset .tabbed-labels > label {
transition: none;
}
}
Screen Reader Support¶
// Enhanced screen reader support
class TabAccessibility {
constructor() {
this.setupAriaAttributes();
this.setupKeyboardSupport();
this.setupScreenReaderAnnouncements();
}
setupAriaAttributes() {
document.querySelectorAll('.tabbed-set').forEach((tabSet, setIndex) => {
const labels = tabSet.querySelectorAll('.tabbed-labels > label');
const contents = tabSet.querySelectorAll('.tabbed-content > .tabbed-block');
labels.forEach((label, index) => {
const contentId = `tabcontent-${setIndex}-${index}`;
const labelId = `tablabel-${setIndex}-${index}`;
label.id = labelId;
label.setAttribute('aria-controls', contentId);
if (contents[index]) {
contents[index].id = contentId;
contents[index].setAttribute('aria-labelledby', labelId);
}
});
});
}
setupScreenReaderAnnouncements() {
document.addEventListener('click', (e) => {
if (e.target.matches('.tabbed-labels > label')) {
this.announceTabChange(e.target);
}
});
}
announceTabChange(label) {
const announcement = document.createElement('div');
announcement.setAttribute('aria-live', 'polite');
announcement.setAttribute('aria-atomic', 'true');
announcement.className = 'sr-only';
announcement.textContent = `Tab ${label.textContent} selected`;
document.body.appendChild(announcement);
setTimeout(() => {
document.body.removeChild(announcement);
}, 1000);
}
}
// Initialize accessibility features
new TabAccessibility();
Integration Examples¶
With Analytics¶
// Google Analytics integration
function trackTabUsage(tabText, tabGroup) {
if (typeof gtag !== 'undefined') {
gtag('event', 'tab_interaction', {
'tab_name': tabText,
'tab_group': tabGroup,
'page_location': window.location.href
});
}
}
// Usage tracking
document.addEventListener('tabChanged', (e) => {
trackTabUsage(e.detail.tabText, 'documentation');
});
With Search Integration¶
// Search highlighting in tabs
function highlightSearchTerms(searchTerm) {
const tabContents = document.querySelectorAll('.tabbed-content');
tabContents.forEach(content => {
const text = content.textContent.toLowerCase();
if (text.includes(searchTerm.toLowerCase())) {
// Activate this tab if it contains search term
const tabSet = content.closest('.tabbed-set');
const index = Array.from(content.parentElement.children).indexOf(content);
const label = tabSet.querySelectorAll('.tabbed-labels > label')[index];
if (label) {
label.click();
}
}
});
}
Troubleshooting¶
Common Issues¶
- Tabs not rendering properly
- Ensure
alternate_style: true
is set - Check that indentation uses 4 spaces
-
Verify empty lines between tab markers
-
Linked tabs not synchronizing
- Enable
content.tabs.link
feature - Ensure tab labels match exactly
-
Check for trailing spaces in labels
-
Styling conflicts
- Clear browser cache
- Verify CSS load order
-
Check for conflicting selectors
-
Accessibility issues
- Ensure ARIA attributes are present
- Test with screen readers
- Verify keyboard navigation works
Debug Mode¶
// Debug tab functionality
const DEBUG_TABS = true;
if (DEBUG_TABS) {
console.log('Tab debugging enabled');
document.addEventListener('click', (e) => {
if (e.target.matches('.tabbed-labels > label')) {
console.log('Tab clicked:', {
text: e.target.textContent,
target: e.target.getAttribute('for'),
timestamp: new Date().toISOString()
});
}
});
}