Skip to content

Tabs

Swaps between panels of content that share one region of the page.

View source View as Markdown

Tabs shows one panel at a time in a shared region, with a list of tabs to switch between them. Useful when several views belong in the same place and the user decides which one to see.

Three panels sharing one region, and one of them at a time.
App.vue
<template>
  <Tabs.Root v-model="tab" class="flex w-72 flex-col gap-4">
    <Tabs.List :class="list" aria-label="Sections">
      <Tabs.Tab
        v-for="entry in entries"
        :key="entry.value"
        :class="trigger"
        :disabled="entry.disabled"
        :value="entry.value"
      >
        {{ entry.label }}
      </Tabs.Tab>

      <Tabs.Indicator :class="indicator" />
    </Tabs.List>

    <Tabs.Content
      v-for="entry in entries"
      :key="entry.value"
      :class="content"
      :value="entry.value"
    >
      {{ entry.body }}
    </Tabs.Content>
  </Tabs.Root>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { Tabs } from '@maas/mirror/vue/Tabs'
const tab = ref('overview')

const entries = [
  {
    value: 'overview',
    label: 'Overview',
    body: 'Three panels sharing one region, and one of them at a time.',
  },
  {
    value: 'tokens',
    label: 'Tokens',
    body: 'Arrow keys move between the tabs and activate as they go.',
  },
  { value: 'archive', label: 'Archive', body: 'Nothing here.', disabled: true },
]

const list =
  'rounded-component-sm border-surface relative isolate flex gap-1 border p-1'

const trigger = [
  'rounded-component-compact-md type-component-sm text-surface-muted relative z-10',
  'flex-1 px-3 py-1.5 transition-colors duration-200 ease-in-out',
  'border-2 border-[transparent] outline outline-4 outline-transparent',
  'focus-visible:focus-ring data-[active=true]:text-surface',
  'enabled:not-data-[active=true]:hover:bg-primary-subtle',
  'enabled:not-data-[active=true]:hover:text-surface',
  'enabled:not-data-[active=true]:active:bg-primary-subtle-hover',
  'data-[disabled=true]:cursor-not-allowed data-[disabled=true]:opacity-40',
].join(' ')

const indicator = [
  'rounded-component-compact-md bg-primary-muted',
  'left-0! translate-x-[var(--mirror-tabs-indicator-left)]',
  'transition-transform duration-200 ease-out',
].join(' ')

const content = 'type-surface-body-md text-surface-muted'
</script>

Usage guidelines

  • Keep every Tabs.Tab inside a Tabs.List, since the list is the roving-focus group and a tab outside it has nothing to rove within.must
  • If a content part is expensive to render, set options.activateOnFocus to false so the arrow keys move focus without activating and Enter or Space commits.should
  • Characters outside A-Z, a-z, 0-9, _ and - are folded to - when the IDs are derived, so give a tab its own elementId where two values differ only in punctuation.must