Skip to content

Tabs ​

Organizes content into multiple sections, displaying only one section at a time while keeping others easily accessible through a tabbed navigation interface. Tabs help structure information without overwhelming the user.

Usage ​

Use tabs to group related content or functionality, allowing users to switch between sections without leaving the current view.

Behavior ​

Every tab lives in its own x-h-tab-item wrapper, which draws the tab's surface. Actions, such as a close button, are <button x-h-tab-action> elements placed beside the tab button inside its item, and a tab can carry several. In a list built with x-for, per-tab actions are added conditionally with <template x-if>.

When the tabs outgrow their list, the list scrolls in place with no visible scrollbar, and the edge that hides more tabs fades out to show where the overflow is. The selected tab is brought into view automatically, both when it first renders and when the selection changes.

A tab bar is either docked or floating. The default docked bar, runs along the edge of the content it sits on, such as the top of a card, a panel or the page. It draws a line along that edge and marks the selected tab with an underline. A floating bar touches no other element. It has its own border, rounded corners and shadow, and the selected tab is raised on its own surface. Set it to floating whenever the bar needs to stands on its own.

Keyboard Handling ​

The tab list is a single Tab stop that lands on the selected tab, so reaching the content does not mean tabbing past every other tab. Once a tab has focus:

  • Right / Left - Moves focus to the next or previous tab, wrapping around at both ends. Horizontal orientation only.
  • Down / Up - Moves focus to the next or previous tab, wrapping around at both ends. Vertical orientation only.
  • Home / End - Moves focus to the first or last tab.
  • Enter / Space - Activates the focused tab.

Tabs disabled with the native disabled attribute are skipped. Tabs with aria-disabled="true" are still reached by the arrows, Home and End, and are announced as unavailable, but neither a click nor Enter activates them. Moving focus does not change the selection, so the tabs can be looked through before one is activated.

A tab's actions share its place in the tab order, so Tab from the focused tab moves through its actions in the order they appear, and Enter / Space activates them like any button. Only the actions of the tab holding the tab stop are reachable this way, which keeps the tab list a single Tab stop. The arrow keys keep moving between tabs while an action has focus.

Accessibility ​

The tab list, tabs and panels get their tablist, tab and tabpanel roles automatically, and the list mirrors data-orientation as aria-orientation so the navigation axis is announced correctly.

Selection stays yours to drive. Bind aria-selected on each tab and hidden on each panel to the same value. A tab written without aria-selected is announced as not selected. The roving tab stop follows the selected tab, so Tab always enters the widget at the tab whose panel is showing.

Each tab needs an id and an aria-controls pointing at its panel, and each panel an id and an aria-labelledby pointing back at its tab, which is what pairs the two for a screen reader. A tab must also live inside a tab item, a tab item inside a tab list, and a tab list inside a tabs root. The component throws when any of this is missing.

The tab item takes role="presentation" and vanishes from the accessibility tree, so the tab and its actions are announced as direct children of the tab list. A tab action is a real button holding only whatever you put inside it, usually an icon, so it needs its own aria-label or aria-labelledby naming the thing it acts on. Activating it does not change the selection. A disabled tab dims its whole item, but its actions stay operable unless you disable them too.

Panels are focusable by default, so Tab moves from the selected tab into its panel and the panel shows a focus ring. That stop is what makes a panel with no focusable content of its own reachable. When your panel already contains something focusable, that element is the way in, so set tabindex="-1" on the panel to avoid a redundant stop.

API Reference ​

Component attribute(s) ​

x-h-tabs
x-h-tab-bar
x-h-tab-list
x-h-tab-item
x-h-tab
x-h-tab-action
x-h-tab-list-actions
x-h-tab-list-action
x-h-tabs-content

Attributes ​

x-h-tabs ​

AttributeTypeRequiredDescription
data-orientationhorizontal
vertical
trueChanges the orientation of the tab list

x-h-tab-bar ​

AttributeTypeRequiredDescription
data-floatingbooleanfalseDetaches the bar with its own border, rounded corners and shadow. Use it for a bar that touches no other element. Without it the bar is docked to the edge it sits on.
data-sizedefault
sm
lg
falseHeight of the tab bar. Ignored when the tab bar is floating or the orientation is vertical.

x-h-tab-item ​

Must not be applied to an interactive element such as a <button> or an <a>.

The required wrapper around every tab. It holds one x-h-tab button and any number of x-h-tab-action buttons, and it draws the tab's surface, so the buttons inside it stay transparent. An item with no actions renders exactly like one with them, and an item whose tab or actions are rendered later, for example behind a <template x-if>, is valid while it waits for them.

x-h-tab ​

AttributeTypeRequiredDescription
idstringtrueIdentifies the tab. The panel's aria-labelledby must point at it.
aria-controlsstringtrueThe id of the tab content this tab shows.
aria-selectedbooleanfalseMarks the tab as selected. Bind it to your own state. Defaults to false when it is left out.
aria-disabledbooleanfalseMarks the tab unavailable while keeping it focusable and announced. It can be reached with the keyboard but not activated.

x-h-tab-action ​

Must be applied to a <button> element inside an x-h-tab-item. A tab can have several actions.

AttributeTypeRequiredDescription
aria-labelstringtrue*Accessible label for the action
aria-labelledbystringtrue*References an element whose text labels the action

INFO

One of aria-label or aria-labelledby is required.

x-h-tabs-content ​

AttributeTypeRequiredDescription
idstringtrueThe id the tab's aria-controls points at.
aria-labelledbystringtrueThe id of the tab that labels this content.
hiddenbooleanfalseShow/hide the tab content
tabindexstringfalseDefaults to 0 so the panel is reachable. Set it to -1 when the panel already contains focusable content, to avoid a redundant stop.

x-h-tab-list-action ​

AttributeTypeRequiredDescription
data-variantoutline
transparent
falseChanges the style of the button. Default is outline.

Modifiers ​

x-h-tab-list-actions ​

ModifierDescription
endTab action will be placed at the end of the tab list container.

Data Slots ​

SlotElement
tabsx-h-tabs
tab-barx-h-tab-bar
tab-listx-h-tab-list
tab-itemx-h-tab-item
tabx-h-tab
tab-actionx-h-tab-action
tab-list-actionsx-h-tab-list-actions
tab-list-actionx-h-tab-list-action
tabs-contentx-h-tabs-content

Examples ​

Sizes ​

html
<div x-h-tabs data-orientation="horizontal">
  <div x-h-tab-bar data-size="sm">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button id="smt1" x-h-tab aria-controls="smt1c" aria-selected="true">Tab 1</button>
      </div>
      <div x-h-tab-item>
        <button id="smt2" x-h-tab aria-controls="smt2c">Tab 2</button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="smt1c" aria-labelledby="smt1" hidden></div>
  <div x-h-tabs-content id="smt2c" aria-labelledby="smt2" hidden></div>
</div>
<div x-h-tabs data-orientation="horizontal">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button id="dt1" x-h-tab aria-controls="dt1c" aria-selected="true">Tab 1</button>
      </div>
      <div x-h-tab-item>
        <button id="dt2" x-h-tab aria-controls="dt2c">Tab 2</button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="dt1c" aria-labelledby="dt1" hidden></div>
  <div x-h-tabs-content id="dt2c" aria-labelledby="dt2" hidden></div>
</div>
<div x-h-tabs data-orientation="horizontal">
  <div x-h-tab-bar data-size="lg">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button id="lgt1" x-h-tab aria-controls="lgt1c" aria-selected="true">Tab 1</button>
      </div>
      <div x-h-tab-item>
        <button id="lgt2" x-h-tab aria-controls="lgt2c">Tab 2</button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="lgt1c" aria-labelledby="lgt1" hidden></div>
  <div x-h-tabs-content id="lgt2c" aria-labelledby="lgt2" hidden></div>
</div>

Disabled tabs ​

Tab 2 uses the native disabled attribute, so the arrow keys step straight over it. Tab 3 uses aria-disabled, so the arrows land on it and it is announced as unavailable, but it cannot be activated.

html
<div x-h-tabs data-orientation="horizontal">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button id="dis1" x-h-tab aria-controls="dis1c" aria-selected="true">Tab 1</button>
      </div>
      <div x-h-tab-item>
        <button id="dis2" x-h-tab aria-controls="dis2c" disabled>Tab 2</button>
      </div>
      <div x-h-tab-item>
        <button id="dis3" x-h-tab aria-controls="dis3c" aria-disabled="true">Tab 3</button>
      </div>
      <div x-h-tab-item>
        <button id="dis4" x-h-tab aria-controls="dis4c">Tab 4</button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="dis1c" aria-labelledby="dis1"></div>
  <div x-h-tabs-content id="dis2c" aria-labelledby="dis2" hidden></div>
  <div x-h-tabs-content id="dis3c" aria-labelledby="dis3" hidden></div>
  <div x-h-tabs-content id="dis4c" aria-labelledby="dis4" hidden></div>
</div>

Scrollable tab content ​

html
<div x-h-tabs data-orientation="horizontal" style="height:10rem">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button id="stce" x-h-tab aria-controls="stcec" aria-selected="true">Tab 1</button>
      </div>
    </div>
  </div>
  <div class="relative" x-h-tabs-content id="stcec" aria-labelledby="stce">
    <div class="absolute inset-0 overflow-auto">
      <img src="/harmonia/logo/harmonia.svg" alt="@harmonia" width="240px" />
    </div>
  </div>
</div>

Horizontal tabs ​

html
<div x-h-tabs data-orientation="horizontal" x-data="{ activeTabId: 'hit1' }">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="hit1" aria-controls="hit1c" :aria-selected="activeTabId === 'hit1'" @click="activeTabId = 'hit1'">Tab 1</button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="hit2" aria-controls="hit2c" :aria-selected="activeTabId === 'hit2'" @click="activeTabId = 'hit2'">Tab 2</button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="hit3" aria-controls="hit3c" :aria-selected="activeTabId === 'hit3'" @click="activeTabId = 'hit3'">Tab 3</button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="hit1c" aria-labelledby="hit1" :hidden="activeTabId !== 'hit1'">
    <div class="p-2">Tab 1 Content</div>
  </div>
  <div x-h-tabs-content id="hit2c" aria-labelledby="hit2" :hidden="activeTabId !== 'hit2'">
    <div class="p-2">Tab 2 Content</div>
  </div>
  <div x-h-tabs-content id="hit3c" aria-labelledby="hit3" :hidden="activeTabId !== 'hit3'">
    <div class="p-2">Tab 3 Content</div>
  </div>
</div>

Overflowing tabs ​

When the tabs do not fit, the list scrolls in place and the edge hiding more tabs fades out. Tab 8 starts selected, so the list opens scrolled to it.

html
<div x-h-tabs data-orientation="horizontal" x-data="{ tabs: Array.from({ length: 12 }, (_, i) => `ovh${i + 1}`), activeTabId: 'ovh8' }">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <template x-for="(id, i) in tabs" :key="id">
        <div x-h-tab-item>
          <button x-h-tab :id="id" :aria-controls="`${id}c`" :aria-selected="activeTabId === id" @click="activeTabId = id" x-text="`Tab ${i + 1}`"></button>
        </div>
      </template>
    </div>
  </div>
  <template x-for="(id, i) in tabs" :key="id">
    <div x-h-tabs-content :id="`${id}c`" :aria-labelledby="id" :hidden="activeTabId !== id">
      <div class="p-2" x-text="`Tab ${i + 1} Content`"></div>
    </div>
  </template>
</div>

Overflowing tabs with actions ​

The action buttons keep their place in the bar while the list scrolls independently. Adding a tab selects it, so the list scrolls to reveal it.

html
<div
  x-h-tabs
  data-orientation="horizontal"
  x-data="{
    count: 12,
    tabs: Array.from({ length: 12 }, (_, i) => `ova${i + 1}`),
    activeTabId: 'ova1',
    add() {
      this.tabs.push(`ova${++this.count}`);
      this.activeTabId = `ova${this.count}`;
    },
  }"
>
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <template x-for="(id, i) in tabs" :key="id">
        <div x-h-tab-item>
          <button x-h-tab :id="id" :aria-controls="`${id}c`" :aria-selected="activeTabId === id" @click="activeTabId = id" x-text="`Tab ${i + 1}`"></button>
        </div>
      </template>
    </div>
    <div x-h-tab-list-actions>
      <button x-h-tab-list-action data-variant="transparent" aria-label="add tab button" @click="add()">
        <svg x-h-lucide role="presentation" data-lucide="plus"></svg>
      </button>
    </div>
  </div>
  <template x-for="(id, i) in tabs" :key="id">
    <div x-h-tabs-content :id="`${id}c`" :aria-labelledby="id" :hidden="activeTabId !== id">
      <div class="p-2" x-text="`Tab ${i + 1} Content`"></div>
    </div>
  </template>
</div>

Horizontal tabs with icon and close button ​

html
<div
  x-h-tabs
  data-orientation="horizontal"
  x-data="{
    tabs: [{ id: 'hib1', title: 'Tab 1' }, { id: 'hib2', title: 'Tab 2' }, { id: 'hib3', title: 'Tab 3' }],
    activeTabId: 'hib1',
    close(id) {
      const index = this.tabs.findIndex((tab) => tab.id === id);
      this.tabs.splice(index, 1);
      if (this.activeTabId === id) this.activeTabId = this.tabs[Math.min(index, this.tabs.length - 1)]?.id;
    },
  }"
>
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <template x-for="tab in tabs" :key="tab.id">
        <div x-h-tab-item>
          <button x-h-tab :id="tab.id" :aria-controls="`${tab.id}c`" :aria-selected="activeTabId === tab.id" @click="activeTabId = tab.id">
            <svg x-h-lucide role="presentation" data-lucide="file"></svg>
            <span x-text="tab.title"></span>
          </button>
          <button x-h-tab-action :aria-label="`close ${tab.title}`" @click="close(tab.id)">
            <svg x-h-icon data-icon="close" role="presentation"></svg>
          </button>
        </div>
      </template>
    </div>
  </div>
  <template x-for="tab in tabs" :key="tab.id">
    <div x-h-tabs-content :id="`${tab.id}c`" :aria-labelledby="tab.id" :hidden="activeTabId !== tab.id">
      <div class="p-2" x-text="`${tab.title} Content`"></div>
    </div>
  </template>
</div>

Horizontal tabs with actions ​

html
<div x-h-tabs data-orientation="horizontal">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="hitwa1" aria-controls="hitwa1c" aria-selected="true">Tab 1</button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
    <div x-h-tab-list-actions>
      <button x-h-tab-list-action data-variant="transparent" aria-label="add tab button">
        <svg x-h-lucide role="presentation" data-lucide="plus"></svg>
      </button>
    </div>
  </div>
  <div x-h-tabs-content id="hitwa1c" aria-labelledby="hitwa1">
    <div class="p-2">Tab 1 Content</div>
  </div>
</div>

Horizontal tabs with actions (end) ​

html
<div x-h-tabs data-orientation="horizontal">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="hitwae1" aria-controls="hitwae1c" aria-selected="true">Tab 1</button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
    <div x-h-tab-list-actions.end>
      <button x-h-tab-list-action aria-label="tab bar menu button">
        <svg x-h-lucide role="presentation" data-lucide="ellipsis"></svg>
      </button>
    </div>
  </div>
  <div x-h-tabs-content id="hitwae1c" aria-labelledby="hitwae1">
    <div class="p-2">Tab 1 Content</div>
  </div>
</div>

Horizontal float tabs ​

html
<div x-h-tabs data-orientation="horizontal" x-data="{ activeTabId: 'ht1' }">
  <div x-h-tab-bar data-floating="true">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="ht1" aria-controls="ht1c" :aria-selected="activeTabId === 'ht1'" @click="activeTabId = 'ht1'">Tab 1</button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="ht2" aria-controls="ht2c" :aria-selected="activeTabId === 'ht2'" @click="activeTabId = 'ht2'">Tab 2</button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="ht3" aria-controls="ht3c" :aria-selected="activeTabId === 'ht3'" @click="activeTabId = 'ht3'">Tab 3</button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="ht1c" aria-labelledby="ht1" :hidden="activeTabId !== 'ht1'">
    <div class="p-2">Tab 1 Content</div>
  </div>
  <div x-h-tabs-content id="ht2c" aria-labelledby="ht2" :hidden="activeTabId !== 'ht2'">
    <div class="p-2">Tab 2 Content</div>
  </div>
  <div x-h-tabs-content id="ht3c" aria-labelledby="ht3" :hidden="activeTabId !== 'ht3'">
    <div class="p-2">Tab 3 Content</div>
  </div>
</div>

Horizontal float tabs that fit to size ​

Add the w-max class to a floating tab bar to make it only as wide as its tabs. Keep data-floating="true" on the bar. A docked bar always spans the edge it sits on, so it does not fit to size.

html
<div x-h-tabs data-orientation="horizontal">
  <div x-h-tab-bar data-floating="true" class="w-max">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="htfts1" aria-controls="htfts1c" aria-selected="true">Sign In</button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="htfts2" aria-controls="htfts2c">Sign Up</button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="htfts1c" aria-labelledby="htfts1">
    <div class="p-2">Sign In</div>
  </div>
  <div x-h-tabs-content id="htfts2c" aria-labelledby="htfts2" hidden="true">
    <div class="p-2">Sign Up</div>
  </div>
</div>

Horizontal float tabs with icon and close button ​

html
<div x-h-tabs data-orientation="horizontal" x-data="{ activeTabId: 'hbt1' }">
  <div x-h-tab-bar data-floating="true">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="hbt1" aria-controls="hbt1c" :aria-selected="activeTabId === 'hbt1'" @click="activeTabId = 'hbt1'">
          <svg x-h-lucide role="presentation" data-lucide="file"></svg>
          Tab 1
        </button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="hbt2" aria-controls="hbt2c" :aria-selected="activeTabId === 'hbt2'" @click="activeTabId = 'hbt2'">
          <svg x-h-lucide role="presentation" data-lucide="file"></svg>
          Tab 2
        </button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="hbt3" aria-controls="hbt3c" :aria-selected="activeTabId === 'hbt3'" @click="activeTabId = 'hbt3'">
          <svg x-h-lucide role="presentation" data-lucide="file"></svg>
          Tab 3
        </button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="hbt1c" aria-labelledby="hbt1" :hidden="activeTabId !== 'hbt1'">
    <div class="p-2">Tab 1 Content</div>
  </div>
  <div x-h-tabs-content id="hbt2c" aria-labelledby="hbt2" :hidden="activeTabId !== 'hbt2'">
    <div class="p-2">Tab 2 Content</div>
  </div>
  <div x-h-tabs-content id="hbt3c" aria-labelledby="hbt3" :hidden="activeTabId !== 'hbt3'">
    <div class="p-2">Tab 3 Content</div>
  </div>
</div>

Horizontal float tabs with actions ​

html
<div x-h-tabs data-orientation="horizontal">
  <div x-h-tab-bar data-floating="true">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="hftwa1" aria-controls="hftwa1c" aria-selected="true">Tab 1</button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
    <div x-h-tab-list-actions>
      <button x-h-tab-list-action data-variant="transparent" aria-label="add tab button">
        <svg x-h-lucide role="presentation" data-lucide="plus"></svg>
      </button>
    </div>
  </div>
  <div x-h-tabs-content id="hftwa1c" aria-labelledby="hftwa1">
    <div class="p-2">Tab 1 Content</div>
  </div>
</div>

Horizontal float tabs with actions (end) ​

html
<div x-h-tabs data-orientation="horizontal">
  <div x-h-tab-bar data-floating="true">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="hftwae1" aria-controls="hftwae1c" aria-selected="true">Tab 1</button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
    <div x-h-tab-list-actions.end>
      <button x-h-tab-list-action aria-label="menu button">
        <svg x-h-lucide role="presentation" data-lucide="ellipsis"></svg>
      </button>
    </div>
  </div>
  <div x-h-tabs-content id="hftwae1c" aria-labelledby="hftwae1">
    <div class="p-2">Tab 1 Content</div>
  </div>
</div>

Vertical tabs ​

html
<div x-h-tabs data-orientation="vertical" x-data="{ activeTabId: 'vit1' }">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="vit1" aria-controls="vit1c" :aria-selected="activeTabId === 'vit1'" @click="activeTabId = 'vit1'">Tab 1</button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="vit2" aria-controls="vit2c" :aria-selected="activeTabId === 'vit2'" @click="activeTabId = 'vit2'">Tab 2</button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="vit3" aria-controls="vit3c" :aria-selected="activeTabId === 'vit3'" @click="activeTabId = 'vit3'">Tab 3</button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="vit1c" aria-labelledby="vit1" :hidden="activeTabId !== 'vit1'">
    <div class="p-2">Tab 1 Content</div>
  </div>
  <div x-h-tabs-content id="vit2c" aria-labelledby="vit2" :hidden="activeTabId !== 'vit2'">
    <div class="p-2">Tab 2 Content</div>
  </div>
  <div x-h-tabs-content id="vit3c" aria-labelledby="vit3" :hidden="activeTabId !== 'vit3'">
    <div class="p-2">Tab 3 Content</div>
  </div>
</div>

Overflowing vertical tabs ​

A vertical list scrolls once something constrains its height, here a fixed height on the root. The edge hiding more tabs fades out, and Tab 6 starts selected, so the list opens scrolled to it.

html
<div x-h-tabs data-orientation="vertical" style="height: 8rem" x-data="{ tabs: Array.from({ length: 10 }, (_, i) => `ovv${i + 1}`), activeTabId: 'ovv6' }">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <template x-for="(id, i) in tabs" :key="id">
        <div x-h-tab-item>
          <button x-h-tab :id="id" :aria-controls="`${id}c`" :aria-selected="activeTabId === id" @click="activeTabId = id" x-text="`Tab ${i + 1}`"></button>
        </div>
      </template>
    </div>
  </div>
  <template x-for="(id, i) in tabs" :key="id">
    <div x-h-tabs-content :id="`${id}c`" :aria-labelledby="id" :hidden="activeTabId !== id">
      <div class="p-2" x-text="`Tab ${i + 1} Content`"></div>
    </div>
  </template>
</div>

Vertical tabs with icon and close button ​

html
<div x-h-tabs data-orientation="vertical" x-data="{ activeTabId: 'vib1' }">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="vib1" aria-controls="vib1c" :aria-selected="activeTabId === 'vib1'" @click="activeTabId = 'vib1'">
          <svg x-h-lucide role="presentation" data-lucide="file"></svg>
          Tab 1
        </button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="vib2" aria-controls="vib2c" :aria-selected="activeTabId === 'vib2'" @click="activeTabId = 'vib2'">
          <svg x-h-lucide role="presentation" data-lucide="file"></svg>
          Tab 2
        </button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="vib3" aria-controls="vib3c" :aria-selected="activeTabId === 'vib3'" @click="activeTabId = 'vib3'">
          <svg x-h-lucide role="presentation" data-lucide="file"></svg>
          Tab 3
        </button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="vib1c" aria-labelledby="vib1" :hidden="activeTabId !== 'vib1'">
    <div class="p-2">Tab 1 Content</div>
  </div>
  <div x-h-tabs-content id="vib2c" aria-labelledby="vib2" :hidden="activeTabId !== 'vib2'">
    <div class="p-2">Tab 2 Content</div>
  </div>
  <div x-h-tabs-content id="vib3c" aria-labelledby="vib3" :hidden="activeTabId !== 'vib3'">
    <div class="p-2">Tab 3 Content</div>
  </div>
</div>

Vertical tabs with actions ​

html
<div x-h-tabs data-orientation="vertical" style="height:8rem">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="vitwa1" aria-controls="vitwa1c" aria-selected="true">Tab 1</button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
    <div x-h-tab-list-actions>
      <button x-h-tab-list-action data-variant="transparent" aria-label="add tab button">
        <svg x-h-lucide role="presentation" data-lucide="plus"></svg>
      </button>
    </div>
  </div>
  <div x-h-tabs-content id="vitwa1c" aria-labelledby="vitwa1">
    <div class="p-2">Tab 1 Content</div>
  </div>
</div>

Vertical tabs with actions (end) ​

html
<div x-h-tabs data-orientation="vertical" style="height:8rem">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="vitwae1" aria-controls="vitwae1c" aria-selected="true">Tab 1</button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
    <div x-h-tab-list-actions.end>
      <button x-h-tab-list-action aria-label="menu button">
        <svg x-h-lucide role="presentation" data-lucide="ellipsis"></svg>
      </button>
    </div>
  </div>
  <div x-h-tabs-content id="vitwae1c" aria-labelledby="vitwae1">
    <div class="p-2">Tab 1 Content</div>
  </div>
</div>

Vertical float tabs ​

html
<div x-h-tabs data-orientation="vertical" x-data="{ activeTabId: 'vt1' }">
  <div x-h-tab-bar data-floating="true">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="vt1" aria-controls="vt1c" :aria-selected="activeTabId === 'vt1'" @click="activeTabId = 'vt1'">Tab 1</button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="vt2" aria-controls="vt2c" :aria-selected="activeTabId === 'vt2'" @click="activeTabId = 'vt2'">Tab 2</button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="vt3" aria-controls="vt3c" :aria-selected="activeTabId === 'vt3'" @click="activeTabId = 'vt3'">Tab 3</button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="vt1c" aria-labelledby="vt1" :hidden="activeTabId !== 'vt1'">
    <div class="p-2">Tab 1 Content</div>
  </div>
  <div x-h-tabs-content id="vt2c" aria-labelledby="vt2" :hidden="activeTabId !== 'vt2'">
    <div class="p-2">Tab 2 Content</div>
  </div>
  <div x-h-tabs-content id="vt3c" aria-labelledby="vt3" :hidden="activeTabId !== 'vt3'">
    <div class="p-2">Tab 3 Content</div>
  </div>
</div>

Vertical float tabs with icon and close button ​

html
<div x-h-tabs data-orientation="vertical" x-data="{ activeTabId: 'vbt1' }">
  <div x-h-tab-bar data-floating="true">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="vbt1" aria-controls="vbt1c" :aria-selected="activeTabId === 'vbt1'" @click="activeTabId = 'vbt1'">
          <svg x-h-lucide role="presentation" data-lucide="file"></svg>
          Tab 1
        </button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="vbt2" aria-controls="vbt2c" :aria-selected="activeTabId === 'vbt2'" @click="activeTabId = 'vbt2'">
          <svg x-h-lucide role="presentation" data-lucide="file"></svg>
          Tab 2
        </button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="vbt3" aria-controls="vbt3c" :aria-selected="activeTabId === 'vbt3'" @click="activeTabId = 'vbt3'">
          <svg x-h-lucide role="presentation" data-lucide="file"></svg>
          Tab 3
        </button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="vbt1c" aria-labelledby="vbt1" :hidden="activeTabId !== 'vbt1'">
    <div class="p-2">Tab 1 Content</div>
  </div>
  <div x-h-tabs-content id="vbt2c" aria-labelledby="vbt2" :hidden="activeTabId !== 'vbt2'">
    <div class="p-2">Tab 2 Content</div>
  </div>
  <div x-h-tabs-content id="vbt3c" aria-labelledby="vbt3" :hidden="activeTabId !== 'vbt3'">
    <div class="p-2">Tab 3 Content</div>
  </div>
</div>

Vertical float tabs with actions ​

html
<div x-h-tabs data-orientation="vertical" style="height:8rem">
  <div x-h-tab-bar data-floating="true">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="vftwa1" aria-controls="vftwa1c" aria-selected="true">Tab 1</button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
    <div x-h-tab-list-actions>
      <button x-h-tab-list-action data-variant="transparent" aria-label="add tab button">
        <svg x-h-lucide role="presentation" data-lucide="plus"></svg>
      </button>
    </div>
  </div>
  <div x-h-tabs-content id="vftwa1c" aria-labelledby="vftwa1">
    <div class="p-2">Tab 1 Content</div>
  </div>
</div>

Vertical float tabs with actions (end) ​

html
<div x-h-tabs data-orientation="vertical" style="height:8rem">
  <div x-h-tab-bar data-floating="true">
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="vftwae1" aria-controls="vftwae1c" aria-selected="true">Tab 1</button>
        <button x-h-tab-action aria-label="close tab">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
    <div x-h-tab-list-actions.end>
      <button x-h-tab-list-action data-variant="outline" aria-label="menu button">
        <svg x-h-lucide role="presentation" data-lucide="ellipsis"></svg>
      </button>
    </div>
  </div>
  <div x-h-tabs-content id="vftwae1c" aria-labelledby="vftwae1">
    <div class="p-2">Tab 1 Content</div>
  </div>
</div>

Tabs with multiple actions ​

A tab can carry several actions, here a menu beside the close button. The popover panel goes inside the tab item, after the actions.

html
<div x-h-tabs data-orientation="horizontal">
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <div x-h-tab-item>
        <button x-h-tab id="mat1" aria-controls="mat1c" aria-selected="true">Tab 1</button>
        <button x-h-tab-action x-h-popover-trigger aria-label="tab 1 menu">
          <svg x-h-lucide role="presentation" data-lucide="ellipsis"></svg>
        </button>
        <button x-h-tab-action aria-label="close tab 1">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
        <div class="w-40 p-2" x-h-popover>Tab menu</div>
      </div>
      <div x-h-tab-item>
        <button x-h-tab id="mat2" aria-controls="mat2c">Tab 2</button>
        <button x-h-tab-action aria-label="close tab 2">
          <svg x-h-icon data-icon="close" role="presentation"></svg>
        </button>
      </div>
    </div>
  </div>
  <div x-h-tabs-content id="mat1c" aria-labelledby="mat1">
    <div class="p-2">Tab 1 Content</div>
  </div>
  <div x-h-tabs-content id="mat2c" aria-labelledby="mat2" hidden></div>
</div>

Mixed closable tabs ​

An x-for template has a single root, so a list where only some tabs can close keeps the tab item as that root and gates the action with <template x-if>.

html
<div
  x-h-tabs
  data-orientation="horizontal"
  x-data="{
    tabs: [
      { id: 'mx1', title: 'Home', closable: false },
      { id: 'mx2', title: 'Notes', closable: true },
      { id: 'mx3', title: 'Draft', closable: true },
    ],
    activeTabId: 'mx1',
    close(id) {
      const index = this.tabs.findIndex((tab) => tab.id === id);
      this.tabs.splice(index, 1);
      if (this.activeTabId === id) this.activeTabId = this.tabs[Math.min(index, this.tabs.length - 1)]?.id;
    },
  }"
>
  <div x-h-tab-bar>
    <div x-h-tab-list>
      <template x-for="tab in tabs" :key="tab.id">
        <div x-h-tab-item>
          <button x-h-tab :id="tab.id" :aria-controls="`${tab.id}c`" :aria-selected="activeTabId === tab.id" @click="activeTabId = tab.id" x-text="tab.title"></button>
          <template x-if="tab.closable">
            <button x-h-tab-action :aria-label="`close ${tab.title}`" @click="close(tab.id)">
              <svg x-h-icon data-icon="close" role="presentation"></svg>
            </button>
          </template>
        </div>
      </template>
    </div>
  </div>
  <template x-for="tab in tabs" :key="tab.id">
    <div x-h-tabs-content :id="`${tab.id}c`" :aria-labelledby="tab.id" :hidden="activeTabId !== tab.id">
      <div class="p-2" x-text="`${tab.title} Content`"></div>
    </div>
  </template>
</div>