Skip to main content
Vuetify0 v1.0 is here
Vuetify0 Logo
Theme
Mode
Palettes
Accessibility
Vuetify One
Sign in to Vuetify One

Access premium tools across the Vuetify ecosystem — Bin, Play, Studio, and more.

Not a subscriber? See what's included

EmTabs

Edit this page
Report a Bug
Open issues
Copy Markdown

Renders elementIntermediateAug 14, 2026

Segmented tabs that show one panel at a time — a bordered tab strip with automatic or manual activation, horizontal or vertical orientation, and the full tablist keyboard map.

Usage

EmTabs is a compound: the root owns selection, EmTabsList holds the triggers, and each EmTabsItem pairs with the EmTabsPanel that shares its value. Bind v-model to the active tab’s value and the matching panel shows; the rest stay mounted but hidden, so form state inside an inactive panel survives switching away and back.

You rarely need to seed the model. mandatory defaults to 'force', which selects the first non-disabled tab when nothing is selected — an empty shallowRef works, and the strip never renders with no active tab.

The strip renders as a segmented control: adjacent tabs share their borders, only the end caps are rounded, and the selected tab lifts above its neighbors. When the labels outgrow the container the list scrolls horizontally rather than wrapping, so a crowded toolbar degrades to a swipe instead of a second row.

<script setup lang="ts">
  import { EmTabs, EmTabsItem, EmTabsList, EmTabsPanel } from '@paper/emerald'

  import { shallowRef } from 'vue'

  const tab = shallowRef('overview')
</script>

<template>
  <EmTabs v-model="tab">
    <EmTabsList label="Project sections">
      <EmTabsItem value="overview">Overview</EmTabsItem>

      <EmTabsItem value="activity">Activity</EmTabsItem>

      <EmTabsItem value="settings">Settings</EmTabsItem>
    </EmTabsList>

    <EmTabsPanel value="overview">
      A summary of the project — status, owners, and the latest release.
    </EmTabsPanel>

    <EmTabsPanel value="activity">
      Recent commits, comments, and deploys in reverse-chronological order.
    </EmTabsPanel>

    <EmTabsPanel value="settings">
      Project name, visibility, and integrations live here.
    </EmTabsPanel>
  </EmTabs>
</template>

Anatomy

vue
<script setup lang="ts">
  import { EmTabs, EmTabsItem, EmTabsList, EmTabsPanel } from '@paper/emerald'
</script>

<template>
  <EmTabs>
    <EmTabsList>
      <EmTabsItem />
    </EmTabsList>

    <EmTabsPanel />
  </EmTabs>
</template>

Composed on v0

EmTabs wraps v0’s Tabs compound — Tabs.Root, Tabs.List, Tabs.Item and Tabs.Panel — with createStep doing the selection underneath.

Tabs.Root is a pure context provider that renders no element of its own, so EmTabs supplies the wrapper div and stamps data-orientation and data-disabled on it — the one place Emerald writes state attributes itself, because there is no v0 element there to carry them. Everything else is v0’s: Tabs.List renders the tablist, Tabs.Item renders each trigger as a native <button> with the roving tabindex and the keyboard handlers, and Tabs.Panel renders the panel with its hidden state. Emerald adds a class and token styling to each and changes none of the behavior.

That split is what the stylesheet hangs off. The selected and disabled looks are [data-selected] and [data-disabled] rules over attributes v0 already publishes, and the vertical layout keys off the aria-orientation v0 puts on the list — Emerald styles the semantics rather than duplicating them into classes.

The default slot of EmTabs forwards Tabs.Root’s slot props, so first, last, next, prev, step and select are available in the template when you want to drive the tabs from outside the strip.

Examples

Manual activation

activation decides what the arrow keys commit. The default, automatic, selects a tab the moment it receives focus — arrowing across the strip switches panels as you go, which is the recommended behavior when panels are cheap to show.

manual separates the two: arrow keys only move focus, and the reader commits with Enter or Space. Reach for it when displaying a panel is expensive — a chart that re-renders, a query that refires — so that a keyboard user traversing the strip on the way to the last tab does not pay for every panel in between.

The trade is one extra keystroke for keyboard users. Pointer behavior is identical in both modes: a click always selects.

Vertical orientation

orientation="vertical" turns the list into a column sized to its widest label, moves the rounded caps to its top and bottom, and switches the keyboard axis — Arrow Up and Arrow Down walk the tabs, and the horizontal arrows do nothing. The list also announces the change through aria-orientation, so assistive technology tells its user which pair of keys to use.

The root does not lay the column and the panels side by side for you — it renders a plain block wrapper, and the arrangement is your layout. This example wraps the list and panels in a flex row, which is all it takes; the panels already carry an inline-start margin in vertical mode, so the gutter between column and content comes from the system.

Vertical suits settings-style surfaces with many sections and long labels, where a horizontal strip would scroll. Past a dozen or so sections, consider navigation instead of tabs.

Disabled tabs

disabled on an EmTabsItem takes that tab out of play without removing it from the strip. The trigger is a native <button>, so the attribute blocks clicks and focus at the platform level; the tab renders at half opacity and keyboard navigation steps over it — arrowing from Review lands on Published, not on Approval.

Home and End skip disabled tabs too, and mandatory="force" respects them when it picks an initial selection: if the first tab is disabled, the first enabled one is selected instead.

A disabled tab still shows its label, which is the point — the reader sees that Approval exists and is not available yet. When the reason is not obvious from context, say why nearby; the tab itself cannot explain. disabled on the root disables the whole instance at once, for surfaces that load before their data.

Props

PropTypeDefaultDescription
v-modelT | T[]The selected tab’s value. With the default mandatory: 'force', the first non-disabled tab is selected when the model is empty
disabledbooleanfalseDisables every tab in the instance. Sets data-disabled on the wrapper
mandatoryboolean | 'force''force''force' auto-selects the first non-disabled tab; true prevents deselecting the last selected tab; false enforces nothing
circularbooleantrueArrow-key navigation wraps from the last tab to the first and back
orientation'horizontal' | 'vertical''horizontal'Layout direction and keyboard axis. Sets data-orientation on the wrapper
activation'automatic' | 'manual''automatic'automatic selects on focus; manual selects on Enter or Space only
namespacestringWhich v0 Tabs context to provide. Only needed when nesting

Parts

Every part takes namespace, defaulting to v0’s v0:tabs, so the prop only matters when one tabs instance nests inside another.

PartPropsNotes
EmTabsListlabel (string)The tablist. label becomes aria-label — supply it when no visible heading labels the strip
EmTabsItemvalue (string | number, required), disabled (boolean, default false)A trigger, rendered as a native <button>. value pairs it with the panel that has the same one
EmTabsPanelvalue (string | number, required)The content for one tab. Hidden — not unmounted — while its tab is unselected

Each part’s default slot is its content. There are no named slots, and none of the parts expose a template-ref surface.

Accessibility

v0’s parts implement the WAI-ARIA tabs pattern: role="tablist" on the list with aria-orientation, role="tab" triggers carrying aria-selected and aria-controls, and role="tabpanel" panels linked back through aria-labelledby. Each trigger is a native <button>, so activation and disabled behavior come from the platform.

Give the list a label whenever the strip has no visible heading — it becomes the tablist’s aria-label, and without it a screen-reader user hears “tab list” with no idea of what the tabs organize.

Keyboard

The strip is a single tab stop: the selected tab holds tabindex="0" and the rest sit at -1, so Tab enters the strip once and the arrows take over from there.

KeyBehavior
Arrow Right / LeftNext / previous tab when horizontal
Arrow Down / UpNext / previous tab when vertical
Home / EndFirst / last enabled tab
Enter, SpaceSelect the focused tab
TabLeave the strip — to the active panel, which is focusable

In automatic mode the arrows select as they move, so Enter and Space are only ever confirming what focus already did. In manual mode the arrows move focus alone and Enter or Space commits. In both modes, navigation skips disabled tabs and wraps at the ends while circular is on; with circular: false it stops at the edges instead.

There is no Escape handling and no Delete — tabs are not dismissible, and the strip owns nothing modal.

Panels

The active panel carries tabindex="0", so a keyboard user can Tab from the strip directly into the panel’s content even when its first child is not focusable. Inactive panels are hidden but stay mounted — switching tabs does not destroy panel state, and nothing inside a hidden panel is reachable or announced.

Disabled

A disabled tab sets the native disabled attribute plus aria-disabled and data-disabled. Being a real disabled button, it is unfocusable and unclickable, and arrow navigation steps over it — a keyboard user is never parked on a control that does nothing. The cost is the usual one: the tab cannot explain itself, so put the reason somewhere visible when it is not obvious.

Focus

The focus indicator is a two-pixel outline in Emerald’s primary tone, raised above the neighboring tabs’ shared borders so it draws as a complete ring. It uses :focus-visible, so it appears for keyboard focus and not for clicks.

Was this page helpful?

© 2016-1970 Vuetify, LLC
Services
Ctrl+/