APG Patterns

Tabs

A set of layered sections of content, known as tab panels, that display one panel of content at a time.

Demo

Automatic Activation (Default)

Tabs are activated automatically when focused with arrow keys.

This is the overview panel content. It provides a general introduction to the product or service.

Manual Activation

Tabs require Enter or Space to activate after focusing.

Content for tab one. Press Enter or Space to activate tabs.

Vertical Orientation

Tabs arranged vertically with Up/Down arrow navigation.

Configure your application settings here.

Accessibility Features

WAI-ARIA Roles

Role Target Element Description
tablist Container Container for tab elements
tab Each tab Individual tab element
tabpanel Panel Content area for each tab

WAI-ARIA tablist role (opens in new tab)

WAI-ARIA Properties

Attribute Target Values Required Configuration
aria-orientation tablist "horizontal" | "vertical" No orientation prop
aria-controls tab ID reference to associated panel Yes Auto-generated
aria-labelledby tabpanel ID reference to associated tab Yes Auto-generated

WAI-ARIA States

aria-selected

Indicates the currently active tab.

Target tab element
Values true | false
Required Yes
Change Trigger Tab click, Arrow keys (automatic), Enter/Space (manual)
Reference aria-selected (opens in new tab)

Keyboard Support

Key Action
Tab Move focus into/out of the tablist
Arrow Right / Arrow Left Navigate between tabs (horizontal)
Arrow Down / Arrow Up Navigate between tabs (vertical)
Home Move focus to first tab
End Move focus to last tab
Enter / Space Activate tab (manual mode only)

Source Code

Tabs.vue
<template>
  <div :class="containerClass">
    <div
      ref="tablistRef"
      role="tablist"
      :aria-label="label"
      :aria-orientation="orientation"
      :class="tablistClass"
      @keydown="handleKeyDown"
    >
      <button
        v-for="tab in tabs"
        :key="tab.id"
        :ref="(el) => setTabRef(tab.id, el)"
        role="tab"
        type="button"
        :id="`${tablistId}-tab-${tab.id}`"
        :aria-selected="tab.id === selectedId"
        :aria-controls="tab.id === selectedId ? `${tablistId}-panel-${tab.id}` : undefined"
        :tabindex="tab.disabled ? -1 : (tab.id === selectedId ? 0 : -1)"
        :disabled="tab.disabled"
        :class="getTabClass(tab)"
        @click="!tab.disabled && handleTabSelection(tab.id)"
      >
        <span class="apg-tab-label">{{ tab.label }}</span>
      </button>
    </div>

    <div class="apg-tabpanels">
      <div
        v-for="tab in tabs"
        :key="tab.id"
        role="tabpanel"
        :id="`${tablistId}-panel-${tab.id}`"
        :aria-labelledby="`${tablistId}-tab-${tab.id}`"
        :hidden="tab.id !== selectedId"
        :class="getPanelClass(tab)"
        :tabindex="tab.id === selectedId ? 0 : -1"
      >
        <div v-if="tab.content" v-html="tab.content" />
        <slot v-else :name="tab.id" />
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed, watch, onMounted, nextTick } from 'vue'

export interface TabItem {
  id: string
  label: string
  content?: string
  disabled?: boolean
}

export interface TabsProps {
  tabs: TabItem[]
  defaultSelectedId?: string
  orientation?: 'horizontal' | 'vertical'
  activationMode?: 'automatic' | 'manual'
  label?: string
}

const props = withDefaults(defineProps<TabsProps>(), {
  orientation: 'horizontal',
  activationMode: 'automatic',
  label: undefined
})

const emit = defineEmits<{
  selectionChange: [tabId: string]
}>()

const selectedId = ref<string>('')
const focusedIndex = ref<number>(0)
const tablistRef = ref<HTMLElement>()
const tabRefs = ref<Record<string, HTMLButtonElement>>({})
const tablistId = ref('')

onMounted(() => {
  tablistId.value = `tabs-${Math.random().toString(36).substr(2, 9)}`
})

const setTabRef = (id: string, el: unknown) => {
  if (el instanceof HTMLButtonElement) {
    tabRefs.value[id] = el
  }
}

const initializeSelectedTab = () => {
  if (props.tabs.length > 0) {
    const initialTab = props.defaultSelectedId
      ? props.tabs.find(tab => tab.id === props.defaultSelectedId && !tab.disabled)
      : props.tabs.find(tab => !tab.disabled)
    selectedId.value = initialTab?.id || props.tabs[0]?.id

    // focusedIndex ใ‚’้ธๆŠžใ•ใ‚ŒใŸใ‚ฟใƒ–ใฎใ‚คใƒณใƒ‡ใƒƒใ‚ฏใ‚นใซๅŒๆœŸ
    const selectedIndex = availableTabs.value.findIndex(tab => tab.id === selectedId.value)
    if (selectedIndex >= 0) {
      focusedIndex.value = selectedIndex
    }
  }
}

const availableTabs = computed(() => props.tabs.filter(tab => !tab.disabled))

const containerClass = computed(() => {
  return `apg-tabs ${props.orientation === 'vertical' ? 'apg-tabs--vertical' : 'apg-tabs--horizontal'}`
})

const tablistClass = computed(() => {
  return `apg-tablist ${props.orientation === 'vertical' ? 'apg-tablist--vertical' : 'apg-tablist--horizontal'}`
})

const getTabClass = (tab: TabItem) => {
  const classes = ['apg-tab']
  classes.push(props.orientation === 'vertical' ? 'apg-tab--vertical' : 'apg-tab--horizontal')
  if (tab.id === selectedId.value) classes.push('apg-tab--selected')
  if (tab.disabled) classes.push('apg-tab--disabled')
  return classes.join(' ')
}

const getPanelClass = (tab: TabItem) => {
  return `apg-tabpanel ${tab.id === selectedId.value ? 'apg-tabpanel--active' : 'apg-tabpanel--inactive'}`
}

const handleTabSelection = (tabId: string) => {
  selectedId.value = tabId
  emit('selectionChange', tabId)
}

const handleTabFocus = async (index: number) => {
  focusedIndex.value = index
  const tab = availableTabs.value[index]
  if (tab && tabRefs.value[tab.id]) {
    await nextTick()
    tabRefs.value[tab.id].focus()
  }
}

const handleKeyDown = async (event: KeyboardEvent) => {
  const target = event.target
  if (!tablistRef.value || !(target instanceof Node) || !tablistRef.value.contains(target)) {
    return
  }

  let newIndex = focusedIndex.value
  let shouldPreventDefault = false

  switch (event.key) {
    case 'ArrowRight':
    case 'ArrowDown':
      newIndex = (focusedIndex.value + 1) % availableTabs.value.length
      shouldPreventDefault = true
      break

    case 'ArrowLeft':
    case 'ArrowUp':
      newIndex = (focusedIndex.value - 1 + availableTabs.value.length) % availableTabs.value.length
      shouldPreventDefault = true
      break

    case 'Home':
      newIndex = 0
      shouldPreventDefault = true
      break

    case 'End':
      newIndex = availableTabs.value.length - 1
      shouldPreventDefault = true
      break

    case 'Enter':
    case ' ':
      if (props.activationMode === 'manual') {
        const focusedTab = availableTabs.value[focusedIndex.value]
        if (focusedTab) {
          handleTabSelection(focusedTab.id)
        }
      }
      shouldPreventDefault = true
      break
  }

  if (shouldPreventDefault) {
    event.preventDefault()

    if (newIndex !== focusedIndex.value) {
      await handleTabFocus(newIndex)

      if (props.activationMode === 'automatic') {
        const newTab = availableTabs.value[newIndex]
        if (newTab) {
          handleTabSelection(newTab.id)
        }
      }
    }
  }
}

watch(() => props.tabs, initializeSelectedTab, { immediate: true })

watch(selectedId, (newSelectedId) => {
  const selectedIndex = availableTabs.value.findIndex(tab => tab.id === newSelectedId)
  if (selectedIndex >= 0) {
    focusedIndex.value = selectedIndex
  }
})
</script>

Usage

Example
<script setup>
import Tabs from './Tabs.vue';

const tabs = [
  { id: 'tab1', label: 'First', content: 'First panel content' },
  { id: 'tab2', label: 'Second', content: 'Second panel content' },
  { id: 'tab3', label: 'Third', content: 'Third panel content' }
];
</script>

<template>
  <Tabs
    :tabs="tabs"
    label="My tabs"
    @tab-change="(id) => console.log('Tab changed:', id)"
  />
</template>

API

Props

Prop Type Default Description
tabs TabItem[] required Array of tab items with id, label, content
label string - Accessible label for the tablist
defaultTab string first tab ID of the initially selected tab
orientation 'horizontal' | 'vertical' 'horizontal' Tab layout direction
activationMode 'automatic' | 'manual' 'automatic' How tabs are activated

Events

Event Payload Description
tab-change string Emitted when the active tab changes

TabItem Interface

Types
interface TabItem {
  id: string;
  label: string;
  content: string;
  disabled?: boolean;
}

Testing

Tests verify APG compliance across keyboard interaction, ARIA attributes, and accessibility requirements.

Test Categories

High Priority: APG Keyboard Interaction

Test Description
ArrowRight/Left Moves focus between tabs (horizontal)
ArrowDown/Up Moves focus between tabs (vertical orientation)
Home/End Moves focus to first/last tab
Loop navigation Arrow keys loop from last to first and vice versa
Disabled skip Skips disabled tabs during navigation
Automatic activation Tab panel changes on focus (default mode)
Manual activation Enter/Space required to activate tab

High Priority: APG ARIA Attributes

Test Description
role="tablist" Container has tablist role
role="tab" Each tab button has tab role
role="tabpanel" Content panel has tabpanel role
aria-selected Selected tab has aria-selected="true"
aria-controls Tab references its panel via aria-controls
aria-labelledby Panel references its tab via aria-labelledby
aria-orientation Reflects horizontal/vertical orientation

High Priority: Focus Management (Roving Tabindex)

Test Description
tabIndex=0 Selected tab has tabIndex=0
tabIndex=-1 Non-selected tabs have tabIndex=-1
Tab to panel Tab key moves focus from tablist to panel
Panel focusable Panel has tabIndex=0 for focus

Medium Priority: Accessibility

Test Description
axe violations No WCAG 2.1 AA violations (via jest-axe)

Testing Tools

See testing-strategy.md (opens in new tab) for full documentation.

Tabs.test.vue.ts
import { render, screen } from "@testing-library/vue";
import userEvent from "@testing-library/user-event";
import { axe } from "jest-axe";
import { describe, expect, it, vi } from "vitest";
import Tabs from "./Tabs.vue";
import type { TabItem } from "./Tabs.vue";

// ใƒ†ใ‚นใƒˆ็”จใฎใ‚ฟใƒ–ใƒ‡ใƒผใ‚ฟ
const defaultTabs: TabItem[] = [
  { id: "tab1", label: "Tab 1", content: "Content 1" },
  { id: "tab2", label: "Tab 2", content: "Content 2" },
  { id: "tab3", label: "Tab 3", content: "Content 3" },
];

const tabsWithDisabled: TabItem[] = [
  { id: "tab1", label: "Tab 1", content: "Content 1" },
  { id: "tab2", label: "Tab 2", content: "Content 2", disabled: true },
  { id: "tab3", label: "Tab 3", content: "Content 3" },
];

describe("Tabs (Vue)", () => {
  // ๐Ÿ”ด High Priority: APG ๆบ–ๆ‹ ใฎๆ ธๅฟƒ
  describe("APG: ใ‚ญใƒผใƒœใƒผใƒ‰ๆ“ไฝœ (Horizontal)", () => {
    describe("Automatic Activation", () => {
      it("ArrowRight ใงๆฌกใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(Tabs, { props: { tabs: defaultTabs } });

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowRight}");

        const tab2 = screen.getByRole("tab", { name: "Tab 2" });
        expect(tab2).toHaveFocus();
        expect(tab2).toHaveAttribute("aria-selected", "true");
        expect(screen.getByRole("tabpanel")).toHaveTextContent("Content 2");
      });

      it("ArrowLeft ใงๅ‰ใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(Tabs, { props: { tabs: defaultTabs, defaultSelectedId: "tab2" } });

        const tab2 = screen.getByRole("tab", { name: "Tab 2" });
        tab2.focus();

        await user.keyboard("{ArrowLeft}");

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        expect(tab1).toHaveFocus();
        expect(tab1).toHaveAttribute("aria-selected", "true");
      });

      it("ArrowRight ใงๆœ€ๅพŒใ‹ใ‚‰ๆœ€ๅˆใซใƒซใƒผใƒ—ใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(Tabs, { props: { tabs: defaultTabs, defaultSelectedId: "tab3" } });

        const tab3 = screen.getByRole("tab", { name: "Tab 3" });
        tab3.focus();

        await user.keyboard("{ArrowRight}");

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        expect(tab1).toHaveFocus();
        expect(tab1).toHaveAttribute("aria-selected", "true");
      });

      it("ArrowLeft ใงๆœ€ๅˆใ‹ใ‚‰ๆœ€ๅพŒใซใƒซใƒผใƒ—ใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(Tabs, { props: { tabs: defaultTabs } });

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowLeft}");

        const tab3 = screen.getByRole("tab", { name: "Tab 3" });
        expect(tab3).toHaveFocus();
        expect(tab3).toHaveAttribute("aria-selected", "true");
      });

      it("Home ใงๆœ€ๅˆใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(Tabs, { props: { tabs: defaultTabs, defaultSelectedId: "tab3" } });

        const tab3 = screen.getByRole("tab", { name: "Tab 3" });
        tab3.focus();

        await user.keyboard("{Home}");

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        expect(tab1).toHaveFocus();
        expect(tab1).toHaveAttribute("aria-selected", "true");
      });

      it("End ใงๆœ€ๅพŒใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(Tabs, { props: { tabs: defaultTabs } });

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{End}");

        const tab3 = screen.getByRole("tab", { name: "Tab 3" });
        expect(tab3).toHaveFocus();
        expect(tab3).toHaveAttribute("aria-selected", "true");
      });

      it("disabled ใ‚ฟใƒ–ใ‚’ใ‚นใ‚ญใƒƒใƒ—ใ—ใฆๆฌกใฎๆœ‰ๅŠนใชใ‚ฟใƒ–ใซ็งปๅ‹•ใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(Tabs, { props: { tabs: tabsWithDisabled } });

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowRight}");

        const tab3 = screen.getByRole("tab", { name: "Tab 3" });
        expect(tab3).toHaveFocus();
        expect(tab3).toHaveAttribute("aria-selected", "true");
      });
    });

    describe("Manual Activation", () => {
      it("็Ÿขๅฐใ‚ญใƒผใงใƒ•ใ‚ฉใƒผใ‚ซใ‚น็งปๅ‹•ใ™ใ‚‹ใŒใƒ‘ใƒใƒซใฏๅˆ‡ใ‚Šๆ›ฟใ‚ใ‚‰ใชใ„", async () => {
        const user = userEvent.setup();
        render(Tabs, { props: { tabs: defaultTabs, activationMode: "manual" } });

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowRight}");

        const tab2 = screen.getByRole("tab", { name: "Tab 2" });
        expect(tab2).toHaveFocus();
        expect(tab1).toHaveAttribute("aria-selected", "true");
        expect(tab2).toHaveAttribute("aria-selected", "false");
        expect(screen.getByRole("tabpanel")).toHaveTextContent("Content 1");
      });

      it("Enter ใงใƒ•ใ‚ฉใƒผใ‚ซใ‚นไธญใฎใ‚ฟใƒ–ใ‚’้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(Tabs, { props: { tabs: defaultTabs, activationMode: "manual" } });

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowRight}");
        await user.keyboard("{Enter}");

        const tab2 = screen.getByRole("tab", { name: "Tab 2" });
        expect(tab2).toHaveAttribute("aria-selected", "true");
        expect(screen.getByRole("tabpanel")).toHaveTextContent("Content 2");
      });

      it("Space ใงใƒ•ใ‚ฉใƒผใ‚ซใ‚นไธญใฎใ‚ฟใƒ–ใ‚’้ธๆŠžใ™ใ‚‹", async () => {
        const user = userEvent.setup();
        render(Tabs, { props: { tabs: defaultTabs, activationMode: "manual" } });

        const tab1 = screen.getByRole("tab", { name: "Tab 1" });
        tab1.focus();

        await user.keyboard("{ArrowRight}");
        await user.keyboard(" ");

        const tab2 = screen.getByRole("tab", { name: "Tab 2" });
        expect(tab2).toHaveAttribute("aria-selected", "true");
        expect(screen.getByRole("tabpanel")).toHaveTextContent("Content 2");
      });
    });
  });

  describe("APG: ใ‚ญใƒผใƒœใƒผใƒ‰ๆ“ไฝœ (Vertical)", () => {
    it("ArrowDown ใงๆฌกใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
      const user = userEvent.setup();
      render(Tabs, { props: { tabs: defaultTabs, orientation: "vertical" } });

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      tab1.focus();

      await user.keyboard("{ArrowDown}");

      const tab2 = screen.getByRole("tab", { name: "Tab 2" });
      expect(tab2).toHaveFocus();
      expect(tab2).toHaveAttribute("aria-selected", "true");
    });

    it("ArrowUp ใงๅ‰ใฎใ‚ฟใƒ–ใซ็งปๅ‹•ใƒป้ธๆŠžใ™ใ‚‹", async () => {
      const user = userEvent.setup();
      render(Tabs, {
        props: { tabs: defaultTabs, orientation: "vertical", defaultSelectedId: "tab2" },
      });

      const tab2 = screen.getByRole("tab", { name: "Tab 2" });
      tab2.focus();

      await user.keyboard("{ArrowUp}");

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      expect(tab1).toHaveFocus();
      expect(tab1).toHaveAttribute("aria-selected", "true");
    });

    it("ArrowDown ใงๆœ€ๅพŒใ‹ใ‚‰ๆœ€ๅˆใซใƒซใƒผใƒ—ใ™ใ‚‹", async () => {
      const user = userEvent.setup();
      render(Tabs, {
        props: { tabs: defaultTabs, orientation: "vertical", defaultSelectedId: "tab3" },
      });

      const tab3 = screen.getByRole("tab", { name: "Tab 3" });
      tab3.focus();

      await user.keyboard("{ArrowDown}");

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      expect(tab1).toHaveFocus();
      expect(tab1).toHaveAttribute("aria-selected", "true");
    });
  });

  describe("APG: ARIA ๅฑžๆ€ง", () => {
    it('tablist ใŒ role="tablist" ใ‚’ๆŒใค', () => {
      render(Tabs, { props: { tabs: defaultTabs } });
      expect(screen.getByRole("tablist")).toBeInTheDocument();
    });

    it('ๅ„ใ‚ฟใƒ–ใŒ role="tab" ใ‚’ๆŒใค', () => {
      render(Tabs, { props: { tabs: defaultTabs } });
      const tabs = screen.getAllByRole("tab");
      expect(tabs).toHaveLength(3);
    });

    it('ๅ„ใƒ‘ใƒใƒซใŒ role="tabpanel" ใ‚’ๆŒใค', () => {
      render(Tabs, { props: { tabs: defaultTabs } });
      expect(screen.getByRole("tabpanel")).toBeInTheDocument();
    });

    it('้ธๆŠžไธญใ‚ฟใƒ–ใŒ aria-selected="true"ใ€้ž้ธๆŠžใŒ "false"', () => {
      render(Tabs, { props: { tabs: defaultTabs } });
      const tabs = screen.getAllByRole("tab");

      expect(tabs[0]).toHaveAttribute("aria-selected", "true");
      expect(tabs[1]).toHaveAttribute("aria-selected", "false");
      expect(tabs[2]).toHaveAttribute("aria-selected", "false");
    });

    it("้ธๆŠžไธญใ‚ฟใƒ–ใฎ aria-controls ใŒใƒ‘ใƒใƒซ id ใจไธ€่‡ด", () => {
      render(Tabs, { props: { tabs: defaultTabs } });
      const selectedTab = screen.getByRole("tab", { name: "Tab 1" });
      const tabpanel = screen.getByRole("tabpanel");

      const ariaControls = selectedTab.getAttribute("aria-controls");
      expect(ariaControls).toBe(tabpanel.id);
    });

    it("ใƒ‘ใƒใƒซใฎ aria-labelledby ใŒใ‚ฟใƒ– id ใจไธ€่‡ด", () => {
      render(Tabs, { props: { tabs: defaultTabs } });
      const selectedTab = screen.getByRole("tab", { name: "Tab 1" });
      const tabpanel = screen.getByRole("tabpanel");

      const ariaLabelledby = tabpanel.getAttribute("aria-labelledby");
      expect(ariaLabelledby).toBe(selectedTab.id);
    });

    it("aria-orientation ใŒ orientation prop ใ‚’ๅๆ˜ ใ™ใ‚‹", () => {
      render(Tabs, { props: { tabs: defaultTabs } });
      expect(screen.getByRole("tablist")).toHaveAttribute(
        "aria-orientation",
        "horizontal"
      );
    });

    it("vertical orientation ใง aria-orientation=vertical", () => {
      render(Tabs, { props: { tabs: defaultTabs, orientation: "vertical" } });
      expect(screen.getByRole("tablist")).toHaveAttribute(
        "aria-orientation",
        "vertical"
      );
    });
  });

  describe("APG: ใƒ•ใ‚ฉใƒผใ‚ซใ‚น็ฎก็† (Roving Tabindex)", () => {
    it("Automatic: ้ธๆŠžไธญใ‚ฟใƒ–ใฎใฟ tabIndex=0", () => {
      render(Tabs, { props: { tabs: defaultTabs } });
      const tabs = screen.getAllByRole("tab");

      expect(tabs[0]).toHaveAttribute("tabIndex", "0");
      expect(tabs[1]).toHaveAttribute("tabIndex", "-1");
      expect(tabs[2]).toHaveAttribute("tabIndex", "-1");
    });

    it("Tab ใ‚ญใƒผใง tabpanel ใซ็งปๅ‹•ใงใใ‚‹", async () => {
      const user = userEvent.setup();
      render(Tabs, { props: { tabs: defaultTabs } });

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      tab1.focus();

      await user.tab();

      expect(screen.getByRole("tabpanel")).toHaveFocus();
    });

    it("tabpanel ใŒ tabIndex=0 ใงใƒ•ใ‚ฉใƒผใ‚ซใ‚นๅฏ่ƒฝ", () => {
      render(Tabs, { props: { tabs: defaultTabs } });
      const tabpanel = screen.getByRole("tabpanel");
      expect(tabpanel).toHaveAttribute("tabIndex", "0");
    });
  });

  // ๐ŸŸก Medium Priority: ใ‚ขใ‚ฏใ‚ปใ‚ทใƒ“ใƒชใƒ†ใ‚ฃๆคœ่จผ
  describe("ใ‚ขใ‚ฏใ‚ปใ‚ทใƒ“ใƒชใƒ†ใ‚ฃ", () => {
    it("axe ใซใ‚ˆใ‚‹ WCAG 2.1 AA ้•ๅใŒใชใ„", async () => {
      const { container } = render(Tabs, { props: { tabs: defaultTabs } });
      const results = await axe(container);
      expect(results).toHaveNoViolations();
    });
  });

  describe("Props", () => {
    it("defaultSelectedId ใงๅˆๆœŸ้ธๆŠžใ‚ฟใƒ–ใ‚’ๆŒ‡ๅฎšใงใใ‚‹", () => {
      render(Tabs, { props: { tabs: defaultTabs, defaultSelectedId: "tab2" } });

      const tab2 = screen.getByRole("tab", { name: "Tab 2" });
      expect(tab2).toHaveAttribute("aria-selected", "true");
      expect(screen.getByRole("tabpanel")).toHaveTextContent("Content 2");
    });

    it("@selectionChange ใŒใ‚ฟใƒ–้ธๆŠžๆ™‚ใซ็™บ็ซใ™ใ‚‹", async () => {
      const handleSelectionChange = vi.fn();
      const user = userEvent.setup();
      render(Tabs, {
        props: { tabs: defaultTabs, onSelectionChange: handleSelectionChange },
      });

      await user.click(screen.getByRole("tab", { name: "Tab 2" }));

      expect(handleSelectionChange).toHaveBeenCalledWith("tab2");
    });
  });

  describe("็•ฐๅธธ็ณป", () => {
    it("defaultSelectedId ใŒๅญ˜ๅœจใ—ใชใ„ๅ ดๅˆใ€ๆœ€ๅˆใฎใ‚ฟใƒ–ใŒ้ธๆŠžใ•ใ‚Œใ‚‹", () => {
      render(Tabs, { props: { tabs: defaultTabs, defaultSelectedId: "nonexistent" } });

      const tab1 = screen.getByRole("tab", { name: "Tab 1" });
      expect(tab1).toHaveAttribute("aria-selected", "true");
    });
  });
});

Resources