Tabs

Material Design 3 tabs component for organizing content into separate views

Tabs

Tabs organize content across different screens and views. @duskmoon-dev/core provides a flexible tabs component with multiple variants and layouts.

Basic Usage

Basic Tabs

Tab Panels

Tabs with content panels:

Tabs with Panels

Content for Tab 1

Content for Tab 2

Content for Tab 3

Variants

Default (Underline)

Tabs with bottom border indicator:

Default Tabs

Pill Variant

Filled background for active state:

Pill Variant

Tonal Variant

Tonal background for active state:

Tonal Variant

Boxed Variant

Tabs with background container:

Boxed Variant

Color Variants

Primary (Default)

Primary Color

Secondary

Secondary Color

Tertiary

Tertiary Color

Orientation

Horizontal (Default)

Horizontal Tabs

Vertical

Vertical Tabs

Alignment

Start-Aligned (Default)

Start-Aligned

Centered

Centered Tabs

End-Aligned

End-Aligned Tabs

Sizes

Small

Small Tabs

Medium (Default)

Medium Tabs

Large

Large Tabs

Full Width Tabs

Tabs that stretch to fill the container:

Full Width Tabs

Scrollable Tabs

For many tabs that overflow the container:

Scrollable Tabs

With Icons

Icons with Text

Tabs with Icons and Text

Icon-Only Tabs

Icon-Only Tabs

With Badges

Show notification counts:

Tabs with Badges

Disabled Tabs

Disabled Tabs

Best Practices

Use tabs for top-level content organization:

Navigation Structure

Accessibility

  • Use semantic <button> elements for interactive tabs
  • Provide aria-label for icon-only tabs
  • Use aria-selected and ARIA roles for enhanced accessibility
  • Ensure keyboard navigation support

Accessible Tabs

Content 1
Content 2

Tab Count

Limit tabs to avoid overwhelming users:

  • Mobile: 3-5 tabs maximum
  • Desktop: 5-7 tabs maximum
  • Use scrollable tabs if more are needed

Framework Examples

React

import { useState } from 'react';

interface TabsProps {
  defaultTab?: number;
  children: React.ReactNode;
}

export function Tabs({ defaultTab = 0, children }: TabsProps) {
  const [activeTab, setActiveTab] = useState(defaultTab);

  return (
    <div>
      <div className="tabs">
        {React.Children.map(children, (child, index) => (
          <button
            className={`tab ${activeTab === index ? 'tab-active' : ''}`}
            onClick={() => setActiveTab(index)}
          >
            {child.props.label}
          </button>
        ))}
      </div>
      {React.Children.map(children, (child, index) => (
        <div className={`tab-panel ${activeTab === index ? 'tab-panel-show' : ''}`}>
          {child}
        </div>
      ))}
    </div>
  );
}

// Usage
<Tabs defaultTab={0}>
  <div label="Tab 1">Content 1</div>
  <div label="Tab 2">Content 2</div>
  <div label="Tab 3">Content 3</div>
</Tabs>

Vue

<template>
  <div>
    <div class="tabs">
      <button
        v-for="(tab, index) in tabs"
        :key="index"
        :class="['tab', { 'tab-active': activeTab === index }]"
        @click="activeTab = index"
      >
        {{ tab }}
      </button>
    </div>
    <div
      v-for="(_, index) in tabs"
      :key="`panel-${index}`"
      :class="['tab-panel', { 'tab-panel-show': activeTab === index }]"
    >
      <slot :name="`panel-${index}`" />
    </div>
  </div>
</template>

<script setup>
import { ref } from 'vue';

const props = defineProps({
  tabs: {
    type: Array,
    required: true
  },
  defaultTab: {
    type: Number,
    default: 0
  }
});

const activeTab = ref(props.defaultTab);
</script>

<!-- Usage -->
<Tabs :tabs="['Tab 1', 'Tab 2', 'Tab 3']">
  <template #panel-0>Content 1</template>
  <template #panel-1>Content 2</template>
  <template #panel-2>Content 3</template>
</Tabs>

JavaScript (Vanilla)

// Simple tabs functionality
document.querySelectorAll('.tab').forEach((tab, index) => {
  tab.addEventListener('click', () => {
    // Remove active class from all tabs
    document.querySelectorAll('.tab').forEach(t => t.classList.remove('tab-active'));

    // Add active class to clicked tab
    tab.classList.add('tab-active');

    // Hide all panels
    document.querySelectorAll('.tab-panel').forEach(panel => {
      panel.classList.remove('tab-panel-show');
    });

    // Show corresponding panel
    document.querySelectorAll('.tab-panel')[index].classList.add('tab-panel-show');
  });
});

API Reference

Tabs Container Classes

ClassDescription
.tabsBase tabs container (required)
.tabs-pillPill variant with filled active state
.tabs-tonalTonal variant with tonal active state
.tabs-boxedBoxed variant with background container
.tabs-verticalVertical orientation
.tabs-scrollableEnable horizontal scrolling
.tabs-centerCenter-align tabs
.tabs-endEnd-align tabs
.tabs-smSmall size
.tabs-mdMedium size (default)
.tabs-lgLarge size
.tabs-fullFull width tabs

Tab Classes

ClassDescription
.tabIndividual tab button (required)
.tab-activeActive state (primary color)
.tab-active-primaryActive state with primary color
.tab-active-secondaryActive state with secondary color
.tab-active-tertiaryActive state with tertiary color
.tab-iconIcon styling
.tab-icon-onlyIcon-only tab
.tab-badgeBadge styling for notifications

Panel Classes

ClassDescription
.tab-panelContent panel container
.tab-panel-showShow the panel

See Also