Skip to content

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

  1. Tabs not rendering properly
  2. Ensure alternate_style: true is set
  3. Check that indentation uses 4 spaces
  4. Verify empty lines between tab markers

  5. Linked tabs not synchronizing

  6. Enable content.tabs.link feature
  7. Ensure tab labels match exactly
  8. Check for trailing spaces in labels

  9. Styling conflicts

  10. Clear browser cache
  11. Verify CSS load order
  12. Check for conflicting selectors

  13. Accessibility issues

  14. Ensure ARIA attributes are present
  15. Test with screen readers
  16. 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()
      });
    }
  });
}