OTTODesign System

Components

Tab bar

The tab bar lets users switch between different pages or views that belong together.

Configurator

LiveTab bar: layout, icons, badge, divider and selection

Sneaker aus Glattleder mit gepolstertem Schaftrand und flexibler Gummisohle.

Obermaterial: Leder. Innenmaterial: Textil. Laufsohle: Gummi.

4,6 von 5 Sternen aus 128 Bewertungen.

HTML
<oc-tab-bar-v1><oc-tab-item-v1><button>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button>Details</button></oc-tab-item-v1><oc-tab-item-v1><button>Bewertungen</button></oc-tab-item-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">Sneaker aus Glattleder mit gepolstertem Schaftrand und flexibler Gummisohle.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">Obermaterial: Leder. Innenmaterial: Textil. Laufsohle: Gummi.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">4,6 von 5 Sternen aus 128 Bewertungen.</p></oc-tab-panel-v1></oc-tab-bar-v1>

Usage

Anatomy

Sneaker aus Glattleder mit gepolstertem Schaftrand und flexibler Gummisohle.

Obermaterial: Leder. Innenmaterial: Textil. Laufsohle: Gummi.

4,6 von 5 Sternen aus 128 Bewertungen.

Lieferung in 2 bis 4 Werktagen, ab 29 € versandkostenfrei.

123456
  1. Icon (optional)
  2. Label
  3. Badge (optional)
  4. Scroll button (when tabs overflow)
  5. Selected indicator
  6. Tab panel
Liveanatomy
HTML
<div class="anatomy" style="display:block;max-width:360px;margin:40px auto 16px">
<oc-tab-bar-v1><oc-tab-item-v1><button><oc-icon-v1 type="list-view"></oc-icon-v1>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="order"></oc-icon-v1>Details<oc-badge-v2 variant="primary" size="50"></oc-badge-v2></button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="rating-empty"></oc-icon-v1>Bewertungen</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="delivery-flat"></oc-icon-v1>Lieferung</button></oc-tab-item-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">Sneaker aus Glattleder mit gepolstertem Schaftrand und flexibler Gummisohle.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">Obermaterial: Leder. Innenmaterial: Textil. Laufsohle: Gummi.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">4,6 von 5 Sternen aus 128 Bewertungen.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">Lieferung in 2 bis 4 Werktagen, ab 29 € versandkostenfrei.</p></oc-tab-panel-v1></oc-tab-bar-v1>
<span class="anatomy-pin" style="left:12%;top:-14%">1</span><span class="anatomy-pin" style="left:26%;top:-14%">2</span><span class="anatomy-pin" style="left:70%;top:-14%">3</span><span class="anatomy-pin" style="left:93%;top:-14%">4</span><span class="anatomy-pin" style="left:-3%;top:37%">5</span><span class="anatomy-pin" style="left:-3%;top:75%">6</span>
<ol class="anatomy-key"><li data-n="1">Icon (optional)</li><li data-n="2">Label</li><li data-n="3">Badge (optional)</li><li data-n="4">Scroll button (when tabs overflow)</li><li data-n="5">Selected indicator</li><li data-n="6">Tab panel</li></ol>

Variants

For the tab item there are following variants.

  • Default
  • Icon Left
  • Icon Top

All tab items in one tab bar must be the same. Different tab items cannot be mixed together.

Default

Icon left

Icon top

Livedefault, icon left and icon top variants
HTML
<oc-tab-bar-v1><oc-tab-item-v1><button>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button>Details</button></oc-tab-item-v1><oc-tab-item-v1><button>Bewertungen</button></oc-tab-item-v1></oc-tab-bar-v1>
<oc-tab-bar-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Details</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Bewertungen</button></oc-tab-item-v1></oc-tab-bar-v1>
<oc-tab-bar-v1 icon-position="top"><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Details</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Bewertungen</button></oc-tab-item-v1></oc-tab-bar-v1>

Badges in sizes 50 or 100 can also be added to the tab items.

Icon left with badge 50 and 100

Icon top with badge 50 and 100

Livetab items with badge
HTML
<p class="demo-label">Icon left with badge 50 and 100</p>
<oc-tab-bar-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Postfach<oc-badge-v2 variant="primary" size="50"></oc-badge-v2></button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Konto</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Hilfe</button></oc-tab-item-v1></oc-tab-bar-v1>
<oc-tab-bar-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Postfach<oc-badge-v2 variant="primary">1</oc-badge-v2></button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Konto</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Hilfe</button></oc-tab-item-v1></oc-tab-bar-v1>
<p class="demo-label">Icon top with badge 50 and 100</p>
<oc-tab-bar-v1 icon-position="top"><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Postfach<oc-badge-v2 variant="primary" size="50"></oc-badge-v2></button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Konto</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Hilfe</button></oc-tab-item-v1></oc-tab-bar-v1>
<oc-tab-bar-v1 icon-position="top"><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Postfach<oc-badge-v2 variant="primary">1</oc-badge-v2></button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Konto</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="smiley-positive"></oc-icon-v1>Hilfe</button></oc-tab-item-v1></oc-tab-bar-v1>

You can add a tab panel directly beneath the tab bar to hold your content.

Sneaker aus Glattleder mit gepolstertem Schaftrand und flexibler Gummisohle.

Obermaterial: Leder. Innenmaterial: Textil. Laufsohle: Gummi.

4,6 von 5 Sternen aus 128 Bewertungen.

Livetab panel
HTML
<oc-tab-bar-v1><oc-tab-item-v1><button>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button>Details</button></oc-tab-item-v1><oc-tab-item-v1><button>Bewertungen</button></oc-tab-item-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100">Sneaker aus Glattleder mit gepolstertem Schaftrand und flexibler Gummisohle.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100">Obermaterial: Leder. Innenmaterial: Textil. Laufsohle: Gummi.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100">4,6 von 5 Sternen aus 128 Bewertungen.</p></oc-tab-panel-v1></oc-tab-bar-v1>

Behavior

States

Selected and enabled (hover, press or focus a tab)

Livestates
HTML
<oc-tab-bar-v1><oc-tab-item-v1><button>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button>Details</button></oc-tab-item-v1><oc-tab-item-v1><button>Bewertungen</button></oc-tab-item-v1></oc-tab-bar-v1>

Placement

Always put the tab bar at the top of the content it controls.

Responsiveness

Breakpoint S (mobile):

By default, the tab items are fill-parent. If the label is too long, it will be abbreviated. If there are too many tab items to fit into the viewport, they will shrink to fit-content. The tab bar then becomes scrollable and the sides fade out to indicate that more tab items are available.

fill-parent: at breakpoint S (phone) long labels are abbreviated

fit-content: too many tabs, the bar scrolls and the sides fade

Liveresponsiveness mobile
HTML
<p class="demo-label">fill-parent: at breakpoint S (phone) long labels are abbreviated</p><div style="background:#fff;max-width:340px"><oc-tab-bar-v1><oc-tab-item-v1><button>Produktbeschreibung und Details</button></oc-tab-item-v1><oc-tab-item-v1><button>Bewertungen</button></oc-tab-item-v1></oc-tab-bar-v1></div></div>
<p class="demo-label">fit-content: too many tabs, the bar scrolls and the sides fade</p><div style="background:#fff;max-width:340px"><oc-tab-bar-v1><oc-tab-item-v1><button>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button>Details</button></oc-tab-item-v1><oc-tab-item-v1><button>Bewertungen</button></oc-tab-item-v1><oc-tab-item-v1><button>Lieferung</button></oc-tab-item-v1><oc-tab-item-v1><button>Größen</button></oc-tab-item-v1><oc-tab-item-v1><button>Pflege</button></oc-tab-item-v1></oc-tab-bar-v1></div></div>
From breakpoint M (desktop):

Tab items are fit-contentby default. If there are too many tab items to fit into the viewport, the sides also fade. On desktop, icon buttons with arrows appear so you can click to scroll through the tab items.

fit-content from breakpoint M

Too many tabs: sides fade, arrow buttons scroll

Liveresponsiveness dektop
HTML
<div style="background:#fff;padding:0 16px 48px"><oc-tab-bar-v1><oc-tab-item-v1><button>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button>Details</button></oc-tab-item-v1></oc-tab-bar-v1></div>
<div style="background:#fff;padding:0 16px 48px"><oc-tab-bar-v1><oc-tab-item-v1><button>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button>Details</button></oc-tab-item-v1><oc-tab-item-v1><button>Bewertungen</button></oc-tab-item-v1><oc-tab-item-v1><button>Lieferung</button></oc-tab-item-v1><oc-tab-item-v1><button>Größen</button></oc-tab-item-v1><oc-tab-item-v1><button>Pflege</button></oc-tab-item-v1><oc-tab-item-v1><button>Material</button></oc-tab-item-v1><oc-tab-item-v1><button>Garantie</button></oc-tab-item-v1><oc-tab-item-v1><button>Zubehör</button></oc-tab-item-v1><oc-tab-item-v1><button>Fragen</button></oc-tab-item-v1><oc-tab-item-v1><button>Service</button></oc-tab-item-v1><oc-tab-item-v1><button>Rücksendung</button></oc-tab-item-v1><oc-tab-item-v1><button>Montage</button></oc-tab-item-v1><oc-tab-item-v1><button>Downloads</button></oc-tab-item-v1></oc-tab-bar-v1></div>

Animation

When you click on a new tab item, the selected indicator moves from the previously selected tab item to the newly selected one. If you select a tab item that is half-hidden off the screen, the tab bar automatically scrolls to put that tab item in the middle of the viewport. The content inside the tab panel slides smoothly in and out when you switch tab items.

Sneaker aus Glattleder mit gepolstertem Schaftrand und flexibler Gummisohle.

Obermaterial: Leder. Innenmaterial: Textil. Laufsohle: Gummi.

4,6 von 5 Sternen aus 128 Bewertungen.

Click a tab: the indicator moves and the panel slides in ↓

Sneaker aus Glattleder mit gepolstertem Schaftrand und flexibler Gummisohle.

Obermaterial: Leder. Innenmaterial: Textil. Laufsohle: Gummi.

4,6 von 5 Sternen aus 128 Bewertungen.

Liveanimation
HTML
<div style="background:#fff;width:100%;max-width:400px"><oc-tab-bar-v1><oc-tab-item-v1><button>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button>Details</button></oc-tab-item-v1><oc-tab-item-v1><button>Bewertungen</button></oc-tab-item-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">Sneaker aus Glattleder mit gepolstertem Schaftrand und flexibler Gummisohle.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">Obermaterial: Leder. Innenmaterial: Textil. Laufsohle: Gummi.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">4,6 von 5 Sternen aus 128 Bewertungen.</p></oc-tab-panel-v1></oc-tab-bar-v1></div>
<p class="demo-label" style="text-align:center">Click a tab: the indicator moves and the panel slides in ↓</p>
<div style="background:#fff;width:100%;max-width:400px"><oc-tab-bar-v1 selected="1"><oc-tab-item-v1><button>Übersicht</button></oc-tab-item-v1><oc-tab-item-v1><button>Details</button></oc-tab-item-v1><oc-tab-item-v1><button>Bewertungen</button></oc-tab-item-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">Sneaker aus Glattleder mit gepolstertem Schaftrand und flexibler Gummisohle.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">Obermaterial: Leder. Innenmaterial: Textil. Laufsohle: Gummi.</p></oc-tab-panel-v1><oc-tab-panel-v1 slot="panel"><p class="oc-copy-100" style="padding:16px 0">4,6 von 5 Sternen aus 128 Bewertungen.</p></oc-tab-panel-v1></oc-tab-bar-v1></div>

Tab bar vs. chips

The tab bar is used for navigation. Clicking a tab item changes the content of a page or the panel below it. Only one tab item can be selected at a time. Chips are used for filtering. Selecting a chip filters or changes content on the same page (like picking colors or sizes). You can select none, one, or multiple chips at the same time.

Do

Meine Rechnungen

LiveDo
HTML
<oc-tab-bar-v1 icon-position="top"><oc-tab-item-v1><button><oc-icon-v1 type="person"></oc-icon-v1>Mein Konto</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="order"></oc-icon-v1>Bestellungen</button></oc-tab-item-v1><oc-tab-item-v1><button><oc-icon-v1 type="order-payment"></oc-icon-v1>Rechnungen</button></oc-tab-item-v1></oc-tab-bar-v1>
<p class="demo-title">Meine Rechnungen</p>
<oc-tab-bar-v1><oc-tab-item-v1><button>Offen</button></oc-tab-item-v1><oc-tab-item-v1><button>Alle</button></oc-tab-item-v1></oc-tab-bar-v1>

Do

Sortieren nach

Farbe

LiveDo
HTML
<p class="demo-title">Sortieren nach</p><div class="demo-row gap-8" style="flex-wrap:nowrap;overflow:hidden"><oc-chip-v3><input type="radio" name="tb-sort" checked aria-label="oc-auto"><label slot="content">Topseller</label></oc-chip-v3><oc-chip-v3><input type="radio" name="tb-sort" aria-label="oc-auto"><label slot="content">Niedrigster Preis</label></oc-chip-v3><oc-chip-v3><input type="radio" name="tb-sort" aria-label="oc-auto"><label slot="content">Höchster Preis</label></oc-chip-v3><oc-chip-v3><input type="radio" name="tb-sort" aria-label="oc-auto"><label slot="content">Neuheiten</label></oc-chip-v3></div></div>
<p class="demo-title">Farbe</p><div class="demo-row gap-8"><oc-chip-v3><input type="checkbox" checked aria-label="oc-auto"><img slot="image" src="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%2712%27 fill=%27%232b6cd6%27/%3E%3C/svg%3E" alt=""><label slot="content">Blau</label></oc-chip-v3><oc-chip-v3><input type="checkbox" aria-label="oc-auto"><img slot="image" src="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%2712%27 fill=%27%23000000%27/%3E%3C/svg%3E" alt=""><label slot="content">Schwarz</label></oc-chip-v3><oc-chip-v3><input type="checkbox" checked aria-label="oc-auto"><img slot="image" src="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%2712%27 fill=%27%23cccccc%27/%3E%3C/svg%3E" alt=""><label slot="content">Grau</label></oc-chip-v3><oc-chip-v3><input type="checkbox" aria-label="oc-auto"><img slot="image" src="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%2712%27 fill=%27%23a8e03a%27/%3E%3C/svg%3E" alt=""><label slot="content">Grün</label></oc-chip-v3><oc-chip-v3><input type="checkbox" aria-label="oc-auto"><img slot="image" src="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%2712%27 fill=%27%23ffd970%27/%3E%3C/svg%3E" alt=""><label slot="content">Gelb</label></oc-chip-v3><oc-chip-v3><input type="checkbox" aria-label="oc-auto"><img slot="image" src="data:image/svg+xml,%3Csvg xmlns=%27http://www.w3.org/2000/svg%27 viewBox=%270 0 24 24%27%3E%3Ccircle cx=%2712%27 cy=%2712%27 r=%2712%27 fill=%27%23e95f80%27/%3E%3C/svg%3E" alt=""><label slot="content">Rot</label></oc-chip-v3></div></div>

Content guidelines

DoKeep the labels short so they do not get abbreviated on mobile.
DoUse the same type of words for all tab items (e.g. all nouns).

Accessibility

To learn how to use the tab bar with a keyboard (like using the arrow keys) or to see code tags like role="tab", please check the technical documentation.

Status

Implementation

Live demo

Mein Konto

Meine Bestellungen

Kommt morgen, Mi., 01.10.Bestellt am 26.09.2026Wird gerade verpacktBestellt am 27.09.2026
Zugestellt am 16.09.Bestellt am 12.09.2026Zugestellt am 04.09.Bestellt am 02.09.2026

Du hast gerade keine offenen Rücksendungen.

Artikel zurückschicken
LiveReal OTTO components, rendered by the OTTO component runtime
HTML
<div class="on-frame" style="padding:32px 16px">
<div style="width:100%;max-width:390px;margin:0 auto;background:#fff;border-radius:16px;overflow:hidden;box-shadow:0 1px 3px rgba(0,0,0,.08)">
<div style="padding:20px 16px 0"><p class="oc-copy-75" style="color:var(--oc-semantic-color-text-secondary)" style="margin:0">Mein Konto</p><p class="oc-headline-200" style="margin:0">Meine Bestellungen</p></div>
<oc-tab-bar-v1 show-divider><oc-tab-item-v1><button>Unterwegs<oc-badge-v2 variant="primary">2</oc-badge-v2></button></oc-tab-item-v1><oc-tab-item-v1><button>Geliefert</button></oc-tab-item-v1><oc-tab-item-v1><button>Retouren</button></oc-tab-item-v1>
  <oc-tab-panel-v1 slot="panel"><div style="padding:0 16px 16px"><oc-row-v2 variant="image" icon-right="arrow-right" hide-divider><img slot="image" src="/previews/imagery/samples/otto-product-still/product-headphones.webp" alt="" width="64" height="64" style="max-width:100%"><a href="#" aria-label="Bestellung vom 26.09.2026 ansehen"></a><span style="display:grid"><span class="oc-copy-100" style="font-weight:700">Kommt morgen, Mi., 01.10.</span><span class="oc-copy-75" style="color:var(--oc-semantic-color-text-secondary)">Bestellt am 26.09.2026</span></span></oc-row-v2><oc-row-v2 variant="image" icon-right="arrow-right"><img slot="image" src="/previews/imagery/samples/otto-product-still/product-sneaker.webp" alt="" width="64" height="64" style="max-width:100%"><a href="#" aria-label="Bestellung vom 27.09.2026 ansehen"></a><span style="display:grid"><span class="oc-copy-100" style="font-weight:700">Wird gerade verpackt</span><span class="oc-copy-75" style="color:var(--oc-semantic-color-text-secondary)">Bestellt am 27.09.2026</span></span></oc-row-v2></div></oc-tab-panel-v1>
  <oc-tab-panel-v1 slot="panel"><div style="padding:0 16px 16px"><oc-row-v2 variant="image" icon-right="arrow-right" hide-divider><img slot="image" src="/previews/imagery/samples/otto-product-still/product-armchair.webp" alt="" width="64" height="64" style="max-width:100%"><a href="#" aria-label="Bestellung vom 12.09.2026 ansehen"></a><span style="display:grid"><span class="oc-copy-100" style="font-weight:700">Zugestellt am 16.09.</span><span class="oc-copy-75" style="color:var(--oc-semantic-color-text-secondary)">Bestellt am 12.09.2026</span></span></oc-row-v2><oc-row-v2 variant="image" icon-right="arrow-right"><img slot="image" src="/previews/references/images/otto/otto-fashion-drop-mojo-06.webp" alt="" width="64" height="64" style="max-width:100%"><a href="#" aria-label="Bestellung vom 02.09.2026 ansehen"></a><span style="display:grid"><span class="oc-copy-100" style="font-weight:700">Zugestellt am 04.09.</span><span class="oc-copy-75" style="color:var(--oc-semantic-color-text-secondary)">Bestellt am 02.09.2026</span></span></oc-row-v2></div></oc-tab-panel-v1>
  <oc-tab-panel-v1 slot="panel"><div style="padding:16px"><p class="oc-copy-100" style="margin:0 0 12px">Du hast gerade keine offenen Rücksendungen.</p><oc-button-v1 variant="secondary" icon-type-left="order-return">Artikel zurückschicken</oc-button-v1></div></oc-tab-panel-v1>
</oc-tab-bar-v1>

Code

Version Tag Status API
v1 <oc-tab-bar-v1> Stable, allowed for generation TabBarV1

Overview (v1)

Tab Bar

The Tab Bar component provides an accessible tab navigation pattern following W3C ARIA standards. It orchestrates tab items and optional panels with full keyboard navigation support.

Note: All three components (oc-tab-bar-v1, oc-tab-item-v1, and oc-tab-panel-v1) are part of the Tab Bar system and live in the same folder.

Default variation
HTML
<oc-tab-bar-v1>
<oc-tab-item-v1>
          <button>Tab 1</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 2</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 3</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 4</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 5</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 6</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 7</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 8</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 9</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 10</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 11</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 12</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 13</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 14</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 15</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 16</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 17</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 18</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 19</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 20</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 21</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 22</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 23</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 24</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 25</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 26</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 27</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 28</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 29</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 30</button>
        </oc-tab-item-v1>
</oc-tab-bar-v1>
Architecture

The tab bar system consists of three tightly-coupled components:

  • <oc-tab-bar-v1>: The orchestrating wrapper with shadow DOM that manages state, keyboard navigation, and ARIA attributes
  • <oc-tab-item-v1>: Lightweight cosmetic wrapper for individual tabs (no shadow DOM for better performance)
  • <oc-tab-panel-v1>: Lightweight cosmetic wrapper for optional content panels (no shadow DOM for better performance)
Performance optimization

The child components (oc-tab-item-v1 and oc-tab-panel-v1) intentionally do not use shadow DOM for optimal performance. They serve as lightweight wrappers that the parent oc-tab-bar-v1 component orchestrates. All interactive elements (buttons/links) are direct children in the light DOM, enabling:

  • Faster rendering without shadow root overhead
  • Direct DOM access for ARIA attribute management
  • Simpler styling through global CSS
  • Reduced JavaScript execution
Configuration

The tab bar component is configurable, allowing you to tailor its features and appearance to your specific needs. To explore all the available options and adjust the component, use the component configurator and see the changes affect the component in real-time.

Usage guidelines

Before integrating the tab bar component into your project, make sure you have correctly installed the OTTO components package. Look through the variations page for examples of possible component variations, including tabs with and without panels, tabs with icons, notification badges, and programmatic control.

Implementation requirements
  • Place tab items first, followed by panels (if used).
  • The order of tab items and panels determines their association (first tab to first panel, second tab to second panel, etc.).
  • Each tab item must contain exactly one interactive element (<button> or <a>) as a direct child. The element must not be nested inside other containers for proper ARIA management.
  • Use <button> for tabs that control content on the current page.
  • Use <a> for tabs that navigate to different pages or URLs.
  • Panels are optional. Tabs can function as pure navigation elements without associated content panels.
Best practices
  • Use the disabled attribute on <oc-tab-item-v1> to prevent selection.
  • Provide meaningful labels on tab buttons for screen readers.
  • Combine icons with text labels for better recognition and accessibility.
  • Use notification badges to indicate status or counts on specific tabs.
  • Tab items and panels are lightweight wrappers without shadow DOM — the parent component manages all ARIA attributes on the interactive elements.
Accessibility

The tab bar component follows the W3C ARIA Tabs Pattern with built-in accessibility features:

Keyboard navigation
Key Action
Tab Moves focus into and out of the tab list
Arrow Left/Up Moves focus to the previous tab (wraps around)
Arrow Right/Down Moves focus to the next tab (wraps around)
Home Moves focus to the first tab
End Moves focus to the last tab

Disabled tabs are automatically skipped during keyboard navigation.

ARIA attributes

The component automatically manages:

  • role="tablist" on the tab bar
  • role="tab" on each tab item's interactive element
  • role="tabpanel" on each panel
  • aria-controls linking tabs to panels
  • aria-selected indicating the current tab
  • aria-labelledby linking panels to tabs
  • tabindex for roving tab index pattern

Configuration (v1)

Tab Bar V1 configuration

Configure the tab bar component with the controls below and see the changes live in the preview canvas. Click the Show code button within the preview canvas to see the source code for the current component configuration.

HTML
<oc-tab-bar-v1>
<oc-tab-item-v1>
          <button>Tab 1</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 2</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 3</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 4</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 5</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 6</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 7</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 8</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 9</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 10</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 11</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 12</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 13</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 14</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 15</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 16</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 17</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 18</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 19</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 20</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 21</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 22</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 23</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 24</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 25</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 26</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 27</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 28</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 29</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 30</button>
        </oc-tab-item-v1>
</oc-tab-bar-v1>

Interactive configurator (Storybook controls); every option is listed in the API section of this file.

API v1

Tab Bar v1 API

API: <oc-tab-bar-v1> (TabBarV1)

The Tab Bar component provides an accessible tab navigation pattern following W3C standards. It orchestrates tab items and optional panels with keyboard navigation and proper ARIA attributes.

Architecture: The parent oc-tab-bar-v1 uses shadow DOM and manages all state and ARIA attributes. The child components (oc-tab-item-v1 and oc-tab-panel-v1) are lightweight wrappers without shadow DOM for optimal performance.

Attributes / properties
Attribute Type Default Required Description
variant "default" | "centered" "default" no Specifies which layout variant to use. Can be one of: default, centered.
icon-position "left" | "top" "left" no Controls the icon position for all tab items. - "left": Icons appear to the left of labels (default) - "top": Icons appear above labels

Important: This setting applies to all tab items in the tab bar. Mixing different icon positions is not supported for visual consistency.
show-divider boolean false no When set to true, shows the divider line below the tab bar.
selected number undefined (first enabled tab) no The index of the initially selected tab (0-based).

Important: This prop only sets the initial state when the component mounts. It is not reactive and will not update the selection if changed later.

For programmatic tab selection after mount, use the select(index) method instead: javascript document.querySelector('oc-tab-bar-v1').select(2);

If not provided, the first enabled tab will be selected. If the specified index is disabled, the first enabled tab will be selected instead.
Slots
Slot Required Description
default yes Contains oc-tab-item-v1 and oc-tab-panel-v1 elements. Tab items should come first, followed by panels (if used). The order determines the tab-to-panel association.
Events
Event Detail type Description
oc-tab-change CustomEvent<{ index: number; tabId: string; }> Fired when a tab is selected. Detail contains the selected tab index.
oc-property-change OcTabBarV1Events["oc-property-change"] Whenever a property value changes, this event triggers. Use this event to track all property changes within the component.

Refer to the Events documentation for more information.
oc-mount { component: string; } Fired when the component is mounted to the DOM. The event is fired when the onMount hook of the component is called by the runtime.
oc-unmount { component: string; } Fired when the component is unmounted from the DOM. The event is fired when the function returned by the onMount hook of the component is called by the runtime.
Methods
  • select: (index: number) => void

    Programmatically select a tab by its index.

    Parameters:

    • index: number - The 0-based index of the tab to select

    Returns: void

    Example:

    const tabBar = document.querySelector('oc-tab-bar-v1');
    tabBar.select(2); // Select the third tab
    

Variations (v1)

Variations

Listed below are the most common variations of the tab bar component as well as specific component variations for different use cases.

You can explore all available options using the component configurator, adjust the component to your needs, and see the changes live in a preview canvas.

Default

Story: components-tab-bar-variations--default · tags: components, tab-bar, 1, variations

The default tab bar with many tabs. At mobile sizes (< 448px), tabs use fill-parent mode (expand equally). At desktop sizes (≥ 448px), tabs use fit-content mode and become scrollable with fade indicators.

Args: defaultSlot=(see snippet)

HTML
<oc-tab-bar-v1>
<oc-tab-item-v1>
          <button>Tab 1</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 2</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 3</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 4</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 5</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 6</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 7</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 8</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 9</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 10</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 11</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 12</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 13</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 14</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 15</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 16</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 17</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 18</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 19</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 20</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 21</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 22</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 23</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 24</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 25</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 26</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 27</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 28</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 29</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 30</button>
        </oc-tab-item-v1>
</oc-tab-bar-v1>
Centered with divider

Story: components-tab-bar-variations--centered-with-divider · tags: components, tab-bar, 1, variations

A centered tab bar with fewer tabs. Tabs are centered within the tab bar container. A divider is also displayed below the tabs.

Args: variant=centered, show-divider=true, defaultSlot=(see snippet)

HTML
<oc-tab-bar-v1 variant="centered" show-divider>
<oc-tab-item-v1>
          <button>Tab 1</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 2</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 3</button>
        </oc-tab-item-v1>
</oc-tab-bar-v1>
With Panels

Story: components-tab-bar-variations--with-panels · tags: components, tab-bar, 1, variations

Tab bar with three tabs and associated content panels. Each tab item is linked to a corresponding panel that displays relevant content. When a tab is selected, its panel becomes visible while others are hidden. The order of tab items and panels determines their association (first tab to first panel, etc.).

Panel 1

This is the content for the first tab.

Panel 2

This is the content for the second tab.

Panel 3

This is the content for the third tab.

HTML
<oc-tab-bar-v1>
<oc-tab-item-v1>
  <button>Overview</button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>Details</button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>Reviews</button>
</oc-tab-item-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <h3 class="oc-headline-100 oc-mb-50">Panel 1</h3>
    <p class="oc-copy-100">This is the content for the first tab.</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <h3 class="oc-headline-100 oc-mb-50">Panel 2</h3>
    <p class="oc-copy-100">This is the content for the second tab.</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <h3 class="oc-headline-100 oc-mb-50">Panel 3</h3>
    <p class="oc-copy-100">This is the content for the third tab.</p>
</oc-tab-panel-v1>
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "With Panels",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <oc-tab-bar-v1>
        <oc-tab-item-v1>
          <button>Overview</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Details</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Reviews</button>
        </oc-tab-item-v1>

        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <h3 class="oc-headline-100 oc-mb-50">Panel 1</h3>
            <p class="oc-copy-100">This is the content for the first tab.</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <h3 class="oc-headline-100 oc-mb-50">Panel 2</h3>
            <p class="oc-copy-100">This is the content for the second tab.</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <h3 class="oc-headline-100 oc-mb-50">Panel 3</h3>
            <p class="oc-copy-100">This is the content for the third tab.</p>
          </div>
        </oc-tab-panel-v1>
      </oc-tab-bar-v1>
    `;
  }
}
Without Panels

Story: components-tab-bar-variations--without-panels · tags: components, tab-bar, 1, variations

Tab bar without content panels. In this configuration, tabs serve purely as navigation elements without displaying additional content. Useful when content is managed elsewhere in the application or when tabs navigate to different pages.

HTML
<oc-tab-bar-v1>
<oc-tab-item-v1>
  <button>Home</button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>Products</button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>About</button>
</oc-tab-item-v1>
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Without Panels",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <oc-tab-bar-v1>
        <oc-tab-item-v1>
          <button>Home</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Products</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>About</button>
        </oc-tab-item-v1>
      </oc-tab-bar-v1>
    `;
  }
}
Mixed - With and Without Panels

Story: components-tab-bar-variations--mixed · tags: components, tab-bar, 1, variations

Tab bar with and without panels.

Privacy Policy

Panel 1

This is the content for the first tab.

Panel 2

This is the content for the second tab.

Panel 3

This is the content for the third tab.

HTML
<oc-tab-bar-v1>
<oc-tab-item-v1>
  <button>Home</button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>Products</button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>About</button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <a href="#">Privacy Policy</a>
</oc-tab-item-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <h3 class="oc-headline-100 oc-mb-50">Panel 1</h3>
    <p class="oc-copy-100">This is the content for the first tab.</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <h3 class="oc-headline-100 oc-mb-50">Panel 2</h3>
    <p class="oc-copy-100">This is the content for the second tab.</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <h3 class="oc-headline-100 oc-mb-50">Panel 3</h3>
    <p class="oc-copy-100">This is the content for the third tab.</p>
</oc-tab-panel-v1>
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Mixed - With and Without Panels",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <oc-tab-bar-v1>
        <oc-tab-item-v1>
          <button>Home</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Products</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>About</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <a href="#">Privacy Policy</a>
        </oc-tab-item-v1>

        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <h3 class="oc-headline-100 oc-mb-50">Panel 1</h3>
            <p class="oc-copy-100">This is the content for the first tab.</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <h3 class="oc-headline-100 oc-mb-50">Panel 2</h3>
            <p class="oc-copy-100">This is the content for the second tab.</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <h3 class="oc-headline-100 oc-mb-50">Panel 3</h3>
            <p class="oc-copy-100">This is the content for the third tab.</p>
          </div>
        </oc-tab-panel-v1>
      </oc-tab-bar-v1>
    `;
  }
}
Initial Selection

Story: components-tab-bar-variations--initial-selection · tags: components, tab-bar, 1, variations

Demonstrates setting the initial selected tab using the selected prop. The second tab (index 1) is selected on mount.

Panel 1

First panel content.

Panel 2

Second panel content (shown initially).

Panel 3

Third panel content.

HTML
<oc-tab-bar-v1 selected="1">
<oc-tab-item-v1>
  <button>Tab 1</button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>Tab 2 (Initially Selected)</button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>Tab 3</button>
</oc-tab-item-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <h3 class="oc-headline-100 oc-mb-50">Panel 1</h3>
    <p class="oc-copy-100">First panel content.</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <h3 class="oc-headline-100 oc-mb-50">Panel 2</h3>
    <p class="oc-copy-100">Second panel content (shown initially).</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <h3 class="oc-headline-100 oc-mb-50">Panel 3</h3>
    <p class="oc-copy-100">Third panel content.</p>
</oc-tab-panel-v1>
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Initial Selection",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <oc-tab-bar-v1 selected="1">
        <oc-tab-item-v1>
          <button>Tab 1</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 2 (Initially Selected)</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Tab 3</button>
        </oc-tab-item-v1>

        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <h3 class="oc-headline-100 oc-mb-50">Panel 1</h3>
            <p class="oc-copy-100">First panel content.</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <h3 class="oc-headline-100 oc-mb-50">Panel 2</h3>
            <p class="oc-copy-100">Second panel content (shown initially).</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <h3 class="oc-headline-100 oc-mb-50">Panel 3</h3>
            <p class="oc-copy-100">Third panel content.</p>
          </div>
        </oc-tab-panel-v1>
      </oc-tab-bar-v1>
    `;
  }
}
Programmatic Selection

Story: components-tab-bar-variations--programmatic-selection · tags: components, tab-bar, 1, variations

Demonstrates programmatic tab selection using the select() method. Use the buttons to change the active tab dynamically.

Select Tab 1 Select Tab 2 Select Tab 3

Panel 1

Programmatically selected via the select(0) method.

Panel 2

Programmatically selected via the select(1) method.

Panel 3

Programmatically selected via the select(2) method.

HTML
<div style="display: flex; flex-direction: column; gap: 16px;">
<div style="display: flex; gap: 8px;">
  <oc-button-v1
    size="50"
    onclick="document.querySelector('#programmatic-tabbar').select(0)"
  >
    Select Tab 1
  </oc-button-v1>
  <oc-button-v1
    size="50"
    onclick="document.querySelector('#programmatic-tabbar').select(1)"
  >
    Select Tab 2
  </oc-button-v1>
  <oc-button-v1
    size="50"
    onclick="document.querySelector('#programmatic-tabbar').select(2)"
  >
    Select Tab 3
  </oc-button-v1>
<oc-tab-bar-v1 id="programmatic-tabbar">
  <oc-tab-item-v1>
    <button>Tab 1</button>
  </oc-tab-item-v1>
  <oc-tab-item-v1>
    <button>Tab 2</button>
  </oc-tab-item-v1>
  <oc-tab-item-v1>
    <button>Tab 3</button>
  </oc-tab-item-v1>
  <oc-tab-panel-v1 slot="panel">
    <div class="oc-p-100">
      <h3 class="oc-headline-100 oc-mb-50">Panel 1</h3>
      <p class="oc-copy-100">Programmatically selected via the select(0) method.</p>
  </oc-tab-panel-v1>
  <oc-tab-panel-v1 slot="panel">
    <div class="oc-p-100">
      <h3 class="oc-headline-100 oc-mb-50">Panel 2</h3>
      <p class="oc-copy-100">Programmatically selected via the select(1) method.</p>
  </oc-tab-panel-v1>
  <oc-tab-panel-v1 slot="panel">
    <div class="oc-p-100">
      <h3 class="oc-headline-100 oc-mb-50">Panel 3</h3>
      <p class="oc-copy-100">Programmatically selected via the select(2) method.</p>
  </oc-tab-panel-v1>
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Programmatic Selection",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <div style="display: flex; flex-direction: column; gap: 16px;">
        <div style="display: flex; gap: 8px;">
          <oc-button-v1
            size="50"
            onclick="document.querySelector('#programmatic-tabbar').select(0)"
          >
            Select Tab 1
          </oc-button-v1>
          <oc-button-v1
            size="50"
            onclick="document.querySelector('#programmatic-tabbar').select(1)"
          >
            Select Tab 2
          </oc-button-v1>
          <oc-button-v1
            size="50"
            onclick="document.querySelector('#programmatic-tabbar').select(2)"
          >
            Select Tab 3
          </oc-button-v1>
        </div>

        <oc-tab-bar-v1 id="programmatic-tabbar">
          <oc-tab-item-v1>
            <button>Tab 1</button>
          </oc-tab-item-v1>
          <oc-tab-item-v1>
            <button>Tab 2</button>
          </oc-tab-item-v1>
          <oc-tab-item-v1>
            <button>Tab 3</button>
          </oc-tab-item-v1>

          <oc-tab-panel-v1 slot="panel">
            <div class="oc-p-100">
              <h3 class="oc-headline-100 oc-mb-50">Panel 1</h3>
              <p class="oc-copy-100">Programmatically selected via the select(0) method.</p>
            </div>
          </oc-tab-panel-v1>
          <oc-tab-panel-v1 slot="panel">
            <div class="oc-p-100">
              <h3 class="oc-headline-100 oc-mb-50">Panel 2</h3>
              <p class="oc-copy-100">Programmatically selected via the select(1) method.</p>
            </div>
          </oc-tab-panel-v1>
          <oc-tab-panel-v1 slot="panel">
            <div class="oc-p-100">
              <h3 class="oc-headline-100 oc-mb-50">Panel 3</h3>
              <p class="oc-copy-100">Programmatically selected via the select(2) method.</p>
            </div>
          </oc-tab-panel-v1>
        </oc-tab-bar-v1>
      </div>
    `;
  }
}
With Disabled Tab

Story: components-tab-bar-variations--with-disabled-tab · tags: components, tab-bar, 1, variations

Tab bar with a disabled tab. Disabled tabs cannot be selected via mouse or keyboard.

First tab content

This panel is not accessible

Third tab content

HTML
<oc-tab-bar-v1>
<oc-tab-item-v1>
  <button>Active Tab</button>
</oc-tab-item-v1>
<oc-tab-item-v1 disabled>
  <button>Disabled Tab</button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>Another Active Tab</button>
</oc-tab-item-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <p class="oc-copy-100">First tab content</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <p class="oc-copy-100">This panel is not accessible</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <p class="oc-copy-100">Third tab content</p>
</oc-tab-panel-v1>
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "With Disabled Tab",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <oc-tab-bar-v1>
        <oc-tab-item-v1>
          <button>Active Tab</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1 disabled>
          <button>Disabled Tab</button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>Another Active Tab</button>
        </oc-tab-item-v1>

        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <p class="oc-copy-100">First tab content</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <p class="oc-copy-100">This panel is not accessible</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <p class="oc-copy-100">Third tab content</p>
          </div>
        </oc-tab-panel-v1>
      </oc-tab-bar-v1>
    `;
  }
}
With Icons

Story: components-tab-bar-variations--with-icons · tags: components, tab-bar, 1, variations

Tabs with icons alongside labels. Icons appear to the left of the text by default. Combine icons with text labels for better recognition and accessibility. Each tab button contains an icon element followed by the label text.

First tab content with icon

Second tab content with icon

Third tab content with icon

HTML
<oc-tab-bar-v1>
<oc-tab-item-v1>
  <button>
    <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
    Label
  </button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>
    <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
    Label
  </button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>
    <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
    Label
  </button>
</oc-tab-item-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <p class="oc-copy-100">First tab content with icon</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <p class="oc-copy-100">Second tab content with icon</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <p class="oc-copy-100">Third tab content with icon</p>
</oc-tab-panel-v1>
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "With Icons",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <oc-tab-bar-v1>
        <oc-tab-item-v1>
          <button>
            <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
            Label
          </button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>
            <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
            Label
          </button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>
            <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
            Label
          </button>
        </oc-tab-item-v1>

        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <p class="oc-copy-100">First tab content with icon</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <p class="oc-copy-100">Second tab content with icon</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <p class="oc-copy-100">Third tab content with icon</p>
          </div>
        </oc-tab-panel-v1>
      </oc-tab-bar-v1>
    `;
  }
}
Icons Top

Story: components-tab-bar-variations--icons-top · tags: components, tab-bar, 1, variations

Tabs with icons positioned at the top of labels. All tabs must use the same icon position for visual consistency.

First tab content with top icon

Second tab content with top icon

Third tab content with top icon

HTML
<oc-tab-bar-v1 icon-position="top">
<oc-tab-item-v1>
  <button>
    <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
    Label
  </button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>
    <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
    Label
  </button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>
    <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
    Label
  </button>
</oc-tab-item-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <p class="oc-copy-100">First tab content with top icon</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <p class="oc-copy-100">Second tab content with top icon</p>
</oc-tab-panel-v1>
<oc-tab-panel-v1 slot="panel">
  <div class="oc-p-100">
    <p class="oc-copy-100">Third tab content with top icon</p>
</oc-tab-panel-v1>
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Icons Top",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <oc-tab-bar-v1 icon-position="top">
        <oc-tab-item-v1>
          <button>
            <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
            Label
          </button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>
            <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
            Label
          </button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>
            <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
            Label
          </button>
        </oc-tab-item-v1>

        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <p class="oc-copy-100">First tab content with top icon</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <p class="oc-copy-100">Second tab content with top icon</p>
          </div>
        </oc-tab-panel-v1>
        <oc-tab-panel-v1 slot="panel">
          <div class="oc-p-100">
            <p class="oc-copy-100">Third tab content with top icon</p>
          </div>
        </oc-tab-panel-v1>
      </oc-tab-bar-v1>
    `;
  }
}
Icon Position Comparison

Story: components-tab-bar-variations--icon-position-comparison · tags: components, tab-bar, 1, variations

Demonstrates the difference between left and top icon positions.

Icon Left

Icon Top

HTML
<div style="display: flex; flex-direction: column; gap: 48px;">
<div>
  <h3 class="oc-headline-100 oc-mb-100">Icon Left</h3>
  <oc-tab-bar-v1>
    <oc-tab-item-v1>
      <button>
        <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
        Label
      </button>
    </oc-tab-item-v1>
    <oc-tab-item-v1>
      <button>
        <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
        Label
      </button>
    </oc-tab-item-v1>
    <oc-tab-item-v1>
      <button>
        <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
        Label
      </button>
    </oc-tab-item-v1>
  </oc-tab-bar-v1>
<div>
  <h3 class="oc-headline-100 oc-mb-100">Icon Top</h3>
  <oc-tab-bar-v1 icon-position="top">
    <oc-tab-item-v1>
      <button>
        <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
        Label
      </button>
    </oc-tab-item-v1>
    <oc-tab-item-v1>
      <button>
        <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
        Label
      </button>
    </oc-tab-item-v1>
    <oc-tab-item-v1>
      <button>
        <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
        Label
      </button>
    </oc-tab-item-v1>
  </oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Icon Position Comparison",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <div style="display: flex; flex-direction: column; gap: 48px;">
        <div>
          <h3 class="oc-headline-100 oc-mb-100">Icon Left</h3>
          <oc-tab-bar-v1>
            <oc-tab-item-v1>
              <button>
                <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
                Label
              </button>
            </oc-tab-item-v1>
            <oc-tab-item-v1>
              <button>
                <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
                Label
              </button>
            </oc-tab-item-v1>
            <oc-tab-item-v1>
              <button>
                <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
                Label
              </button>
            </oc-tab-item-v1>
          </oc-tab-bar-v1>
        </div>

        <div>
          <h3 class="oc-headline-100 oc-mb-100">Icon Top</h3>
          <oc-tab-bar-v1 icon-position="top">
            <oc-tab-item-v1>
              <button>
                <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
                Label
              </button>
            </oc-tab-item-v1>
            <oc-tab-item-v1>
              <button>
                <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
                Label
              </button>
            </oc-tab-item-v1>
            <oc-tab-item-v1>
              <button>
                <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
                Label
              </button>
            </oc-tab-item-v1>
          </oc-tab-bar-v1>
        </div>
      </div>
    `;
  }
}
With Notification Badges

Story: components-tab-bar-variations--with-badges · tags: components, tab-bar, 1, variations

Tabs with notification badges showing counts and status indicators. Badges can be combined with or without icons to show unread counts, status, or alerts. Place the badge element after the label text inside the button. Supports both horizontal (icon left) and vertical (icon top) layouts.

Horizontal

Horizontal (without icons)

Vertical

HTML
<h3 class="oc-headline-100 oc-mb-100">Horizontal</h3>
<oc-tab-bar-v1>
<oc-tab-item-v1>
  <button>
    <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
    Label
  </button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>
    <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
    Label
    <oc-badge-v2 variant="primary" size="50"></oc-badge-v2>
  </button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>
    <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
    Label
    <oc-badge-v2 variant="primary">1</oc-badge-v2>
  </button>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <button>
    <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
    Label
    <oc-badge-v2 variant="success" icon="check"></oc-badge-v2>
  </button>
</oc-tab-item-v1>
</oc-tab-bar-v1>
<div class="oc-mt-200">
<h3 class="oc-headline-100 oc-mb-100">Horizontal (without icons)</h3>
<oc-tab-bar-v1>
  <oc-tab-item-v1>
    <button>Label</button>
  </oc-tab-item-v1>
  <oc-tab-item-v1>
    <button>
      Label
      <oc-badge-v2 variant="primary" size="50"></oc-badge-v2>
    </button>
  </oc-tab-item-v1>
  <oc-tab-item-v1>
    <button>
      Label
      <oc-badge-v2 variant="primary">1</oc-badge-v2>
    </button>
  </oc-tab-item-v1>
  <oc-tab-item-v1>
    <button>
      Label
      <oc-badge-v2 variant="success" icon="check"></oc-badge-v2>
    </button>
  </oc-tab-item-v1>
</oc-tab-bar-v1>
<div class="oc-mt-200">
<h3 class="oc-headline-100 oc-mb-100">Vertical</h3>
<oc-tab-bar-v1 icon-position="top">
  <oc-tab-item-v1>
    <button>
      <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
      Label
    </button>
  </oc-tab-item-v1>
  <oc-tab-item-v1>
    <button>
      <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
      Label
      <oc-badge-v2 variant="primary" size="50"></oc-badge-v2>
    </button>
  </oc-tab-item-v1>
  <oc-tab-item-v1>
    <button>
      <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
      Label
      <oc-badge-v2 variant="primary">1</oc-badge-v2>
    </button>
  </oc-tab-item-v1>
  <oc-tab-item-v1>
    <button>
      <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
      Seeeeeeehr langes label
      <oc-badge-v2 variant="success" icon="check"></oc-badge-v2>
    </button>
  </oc-tab-item-v1>
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "With Notification Badges",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <h3 class="oc-headline-100 oc-mb-100">Horizontal</h3>
      <oc-tab-bar-v1>
        <oc-tab-item-v1>
          <button>
            <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
            Label
          </button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>
            <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
            Label
            <oc-badge-v2 variant="primary" size="50"></oc-badge-v2>
          </button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>
            <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
            Label
            <oc-badge-v2 variant="primary">1</oc-badge-v2>
          </button>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <button>
            <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
            Label
            <oc-badge-v2 variant="success" icon="check"></oc-badge-v2>
          </button>
        </oc-tab-item-v1>
      </oc-tab-bar-v1>

      <div class="oc-mt-200">
        <h3 class="oc-headline-100 oc-mb-100">Horizontal (without icons)</h3>
        <oc-tab-bar-v1>
          <oc-tab-item-v1>
            <button>Label</button>
          </oc-tab-item-v1>
          <oc-tab-item-v1>
            <button>
              Label
              <oc-badge-v2 variant="primary" size="50"></oc-badge-v2>
            </button>
          </oc-tab-item-v1>
          <oc-tab-item-v1>
            <button>
              Label
              <oc-badge-v2 variant="primary">1</oc-badge-v2>
            </button>
          </oc-tab-item-v1>
          <oc-tab-item-v1>
            <button>
              Label
              <oc-badge-v2 variant="success" icon="check"></oc-badge-v2>
            </button>
          </oc-tab-item-v1>
        </oc-tab-bar-v1>
      </div>

      <div class="oc-mt-200">
        <h3 class="oc-headline-100 oc-mb-100">Vertical</h3>
        <oc-tab-bar-v1 icon-position="top">
          <oc-tab-item-v1>
            <button>
              <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
              Label
            </button>
          </oc-tab-item-v1>
          <oc-tab-item-v1>
            <button>
              <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
              Label
              <oc-badge-v2 variant="primary" size="50"></oc-badge-v2>
            </button>
          </oc-tab-item-v1>
          <oc-tab-item-v1>
            <button>
              <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
              Label
              <oc-badge-v2 variant="primary">1</oc-badge-v2>
            </button>
          </oc-tab-item-v1>
          <oc-tab-item-v1>
            <button>
              <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
              Seeeeeeehr langes label
              <oc-badge-v2 variant="success" icon="check"></oc-badge-v2>
            </button>
          </oc-tab-item-v1>
        </oc-tab-bar-v1>
      </div>
    `;
  }
}
Layout Behavior (Responsive)

Story: components-tab-bar-variations--layout-behavior · tags: components, tab-bar, 1, variations

Demonstrates the responsive breakpoint-based layout behavior.

Breakpoint-based (default): Component adapts based on viewport size following cinema's patterns:

  • Mobile S (< 448px): fill-parent - tabs expand equally and may truncate
  • Desktop M+ (≥ 448px): fit-content - tabs use natural width and become scrollable if needed

Use the responsive view controls to see how tabs adapt at different viewport sizes.

Few Tabs

Mobile S: fill-parent (expand equally)
Desktop M+: fit-content (natural width)

Many Tabs

Mobile S: fill-parent (may truncate if too many)
Desktop M+: fit-content (scrollable)

Long Labels

Mobile S: Text truncates with ellipsis when tabs expand equally
Desktop M+: Full text shown, scrollable if needed

With Icons (Top)

HTML
<div style="display: flex; flex-direction: column; gap: 48px;">
<div>
  <h3 class="oc-headline-100 oc-mb-100">Few Tabs</h3>
  <p class="oc-copy-100 oc-mb-100">
    Mobile S: fill-parent (expand equally)<br />
    Desktop M+: fit-content (natural width)
  </p>
  <oc-tab-bar-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
  </oc-tab-bar-v1>
<div>
  <h3 class="oc-headline-100 oc-mb-100">Many Tabs</h3>
  <p class="oc-copy-100 oc-mb-100">
    Mobile S: fill-parent (may truncate if too many)<br />
    Desktop M+: fit-content (scrollable)
  </p>
  <oc-tab-bar-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
  </oc-tab-bar-v1>
<div>
  <h3 class="oc-headline-100 oc-mb-100">Long Labels</h3>
  <p class="oc-copy-100 oc-mb-100">
    Mobile S: Text truncates with ellipsis when tabs expand equally<br />
    Desktop M+: Full text shown, scrollable if needed
  </p>
  <oc-tab-bar-v1>
    <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
    <oc-tab-item-v1
      ><button>Seeeehr Langes Label mit sehr viel Text</button></oc-tab-item-v1
    >
    <oc-tab-item-v1><button>Label Zwei</button></oc-tab-item-v1>
  </oc-tab-bar-v1>
<div>
  <h3 class="oc-headline-100 oc-mb-100">With Icons (Top)</h3>
  <oc-tab-bar-v1 icon-position="top">
    <oc-tab-item-v1>
      <button>
        <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
        Label
      </button>
    </oc-tab-item-v1>
    <oc-tab-item-v1>
      <button>
        <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
        Labelzwei
      </button>
    </oc-tab-item-v1>
    <oc-tab-item-v1>
      <button>
        <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
        Label
      </button>
    </oc-tab-item-v1>
  </oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Layout Behavior (Responsive)",
  parameters: {
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <div style="display: flex; flex-direction: column; gap: 48px;">
        <div>
          <h3 class="oc-headline-100 oc-mb-100">Few Tabs</h3>
          <p class="oc-copy-100 oc-mb-100">
            Mobile S: fill-parent (expand equally)<br />
            Desktop M+: fit-content (natural width)
          </p>
          <oc-tab-bar-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
          </oc-tab-bar-v1>
        </div>

        <div>
          <h3 class="oc-headline-100 oc-mb-100">Many Tabs</h3>
          <p class="oc-copy-100 oc-mb-100">
            Mobile S: fill-parent (may truncate if too many)<br />
            Desktop M+: fit-content (scrollable)
          </p>
          <oc-tab-bar-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
          </oc-tab-bar-v1>
        </div>

        <div>
          <h3 class="oc-headline-100 oc-mb-100">Long Labels</h3>
          <p class="oc-copy-100 oc-mb-100">
            Mobile S: Text truncates with ellipsis when tabs expand equally<br />
            Desktop M+: Full text shown, scrollable if needed
          </p>
          <oc-tab-bar-v1>
            <oc-tab-item-v1><button>Label</button></oc-tab-item-v1>
            <oc-tab-item-v1
              ><button>Seeeehr Langes Label mit sehr viel Text</button></oc-tab-item-v1
            >
            <oc-tab-item-v1><button>Label Zwei</button></oc-tab-item-v1>
          </oc-tab-bar-v1>
        </div>

        <div>
          <h3 class="oc-headline-100 oc-mb-100">With Icons (Top)</h3>
          <oc-tab-bar-v1 icon-position="top">
            <oc-tab-item-v1>
              <button>
                <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
                Label
              </button>
            </oc-tab-item-v1>
            <oc-tab-item-v1>
              <button>
                <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
                Labelzwei
              </button>
            </oc-tab-item-v1>
            <oc-tab-item-v1>
              <button>
                <oc-icon-v1 type="smiley-positive"></oc-icon-v1>
                Label
              </button>
            </oc-tab-item-v1>
          </oc-tab-bar-v1>
        </div>
      </div>
    `;
  }
}
Demo: Multiple Configurations

Story: components-tab-bar-variations--demo · tags: components, tab-bar, 1, variations

Comprehensive demo showing multiple tab bars with different configurations.

Basic Tab Bar

Welcome Home

This is the home page content.

Our Products

Browse our product catalog.

About Us

Learn more about our company.

With Links (Navigation)

Overview Specifications Reviews

With Disabled Tab

HTML
<div style="display: flex; flex-direction: column; gap: 32px;">
<div>
  <h2 class="oc-headline-200 oc-mb-100">Basic Tab Bar</h2>
  <oc-tab-bar-v1>
    <oc-tab-item-v1><button>Home</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Products</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>About</button></oc-tab-item-v1>
    <oc-tab-panel-v1 slot="panel">
      <div class="oc-p-100">
        <h3 class="oc-headline-100 oc-mb-50">Welcome Home</h3>
        <p class="oc-copy-100">This is the home page content.</p>
    </oc-tab-panel-v1>
    <oc-tab-panel-v1 slot="panel">
      <div class="oc-p-100">
        <h3 class="oc-headline-100 oc-mb-50">Our Products</h3>
        <p class="oc-copy-100">Browse our product catalog.</p>
    </oc-tab-panel-v1>
    <oc-tab-panel-v1 slot="panel">
      <div class="oc-p-100">
        <h3 class="oc-headline-100 oc-mb-50">About Us</h3>
        <p class="oc-copy-100">Learn more about our company.</p>
    </oc-tab-panel-v1>
  </oc-tab-bar-v1>
<div>
  <h2 class="oc-headline-200 oc-mb-100">With Links (Navigation)</h2>
  <oc-tab-bar-v1>
    <oc-tab-item-v1><a href="#overview">Overview</a></oc-tab-item-v1>
    <oc-tab-item-v1><a href="#specifications">Specifications</a></oc-tab-item-v1>
    <oc-tab-item-v1><a href="#reviews">Reviews</a></oc-tab-item-v1>
  </oc-tab-bar-v1>
<div>
  <h2 class="oc-headline-200 oc-mb-100">With Disabled Tab</h2>
  <oc-tab-bar-v1>
    <oc-tab-item-v1><button>Available</button></oc-tab-item-v1>
    <oc-tab-item-v1 disabled><button>Coming Soon</button></oc-tab-item-v1>
    <oc-tab-item-v1><button>Active</button></oc-tab-item-v1>
  </oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Multiple Configurations",
  parameters: {
    chromatic: {
      hideInChromatic: true
    },
    controls: {
      disabled: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <div style="display: flex; flex-direction: column; gap: 32px;">
        <div>
          <h2 class="oc-headline-200 oc-mb-100">Basic Tab Bar</h2>
          <oc-tab-bar-v1>
            <oc-tab-item-v1><button>Home</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Products</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>About</button></oc-tab-item-v1>

            <oc-tab-panel-v1 slot="panel">
              <div class="oc-p-100">
                <h3 class="oc-headline-100 oc-mb-50">Welcome Home</h3>
                <p class="oc-copy-100">This is the home page content.</p>
              </div>
            </oc-tab-panel-v1>
            <oc-tab-panel-v1 slot="panel">
              <div class="oc-p-100">
                <h3 class="oc-headline-100 oc-mb-50">Our Products</h3>
                <p class="oc-copy-100">Browse our product catalog.</p>
              </div>
            </oc-tab-panel-v1>
            <oc-tab-panel-v1 slot="panel">
              <div class="oc-p-100">
                <h3 class="oc-headline-100 oc-mb-50">About Us</h3>
                <p class="oc-copy-100">Learn more about our company.</p>
              </div>
            </oc-tab-panel-v1>
          </oc-tab-bar-v1>
        </div>

        <div>
          <h2 class="oc-headline-200 oc-mb-100">With Links (Navigation)</h2>
          <oc-tab-bar-v1>
            <oc-tab-item-v1><a href="#overview">Overview</a></oc-tab-item-v1>
            <oc-tab-item-v1><a href="#specifications">Specifications</a></oc-tab-item-v1>
            <oc-tab-item-v1><a href="#reviews">Reviews</a></oc-tab-item-v1>
          </oc-tab-bar-v1>
        </div>

        <div>
          <h2 class="oc-headline-200 oc-mb-100">With Disabled Tab</h2>
          <oc-tab-bar-v1>
            <oc-tab-item-v1><button>Available</button></oc-tab-item-v1>
            <oc-tab-item-v1 disabled><button>Coming Soon</button></oc-tab-item-v1>
            <oc-tab-item-v1><button>Active</button></oc-tab-item-v1>
          </oc-tab-bar-v1>
        </div>
      </div>
    `;
  }
}
Demo: Mein Konto

Story: components-tab-bar-variations--demo-mein-konto · tags: components, tab-bar, 1, variations

Demo showcasing a "Mein Konto" navigation tab bar with icons. Each tab is a link representing a different section of the user account area.

HTML
<oc-tab-bar-v1>
<oc-tab-item-v1>
  <a href="https://develop.otto.de/myaccount/dashboard" target="_blank">
    <oc-icon-v1 type="person"></oc-icon-v1>
    Mein Konto
  </a>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <a href="https://develop.otto.de/myaccount/orders" target="_blank">
    <oc-icon-v1 type="order"></oc-icon-v1>
    Bestellungen
  </a>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <a href="https://develop.otto.de/myaccount/invoices" target="_blank">
    <oc-icon-v1 type="euro"></oc-icon-v1>
    Rechnungen
  </a>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <a href="https://develop.otto.de/myaccount/up" target="_blank">
    <oc-icon-v1 type="otto-up"></oc-icon-v1>
    Mein UP
  </a>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <a href="https://develop.otto.de/myaccount/inbox" target="_blank">
    <oc-icon-v1 type="chat"></oc-icon-v1>
    Nachrichten
  </a>
</oc-tab-item-v1>
<oc-tab-item-v1>
  <a href="https://develop.otto.de/myaccount/profile" target="_blank">
    <oc-icon-v1 type="settings"></oc-icon-v1>
    Profil
  </a>
</oc-tab-item-v1>
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Mein Konto",
  parameters: {
    controls: {
      disabled: true
    },
    chromatic: {
      hideInChromatic: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <oc-tab-bar-v1>
        <oc-tab-item-v1>
          <a href="https://develop.otto.de/myaccount/dashboard" target="_blank">
            <oc-icon-v1 type="person"></oc-icon-v1>
            Mein Konto
          </a>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <a href="https://develop.otto.de/myaccount/orders" target="_blank">
            <oc-icon-v1 type="order"></oc-icon-v1>
            Bestellungen
          </a>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <a href="https://develop.otto.de/myaccount/invoices" target="_blank">
            <oc-icon-v1 type="euro"></oc-icon-v1>
            Rechnungen
          </a>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <a href="https://develop.otto.de/myaccount/up" target="_blank">
            <oc-icon-v1 type="otto-up"></oc-icon-v1>
            Mein UP
          </a>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <a href="https://develop.otto.de/myaccount/inbox" target="_blank">
            <oc-icon-v1 type="chat"></oc-icon-v1>
            Nachrichten
          </a>
        </oc-tab-item-v1>
        <oc-tab-item-v1>
          <a href="https://develop.otto.de/myaccount/profile" target="_blank">
            <oc-icon-v1 type="settings"></oc-icon-v1>
            Profil
          </a>
        </oc-tab-item-v1>
      </oc-tab-bar-v1>
    `;
  }
}
Demo: Mein Konto with Async Fragments

Story: components-tab-bar-variations--demo-mein-konto-with-async-fragments · tags: components, tab-bar, 1, variations

Demo showcasing a "Mein Konto" navigation tab bar with icons. Each tab is a link representing a different section of the user account area.

<p>
  This demonstrates tab bar with tab panels rendering dynamically loaded extrenal html
  fragments (aka async fragments). Each tab panel fetches its content from a separate URL when
  the tab is selected.
</p>
<p>
  Each Tab Panel can display a loading state while the content is being fetched, which may be
  usefull for devices with slow network connections. In this example, the content is fetched
  from a local server and may take a few seconds to load.
</p>
<p>
  You can test this behavior by ussing developer tools to throttle the network speed to "Slow
  3G" and then clicking on the tabs to see the loading state before the content is displayed.
</p>
<p>
  By default the conten will stay in the dom event when the tab has been deselected. You can
  change this behavior by setting the <code>discard-on-inactive</code> attribute on the tab
  panel which should discard the content when the tab is deselected. The tab panel will then
  restore loading state and repeat the fetch when the tab is selected again.
</p>
<hr />
<oc-tab-bar-v1>
  ${[["person", "Mein Konto"], ["order", "Bestellungen"], ["euro", "Rechnungen"], ["otto-up", "Mein UP"], ["chat", "Nachrichten"], ["settings", "Profil"]].map(([icon, label]) => html`
      <oc-tab-item-v1>
        <button><oc-icon-v1 type="${icon}"></oc-icon-v1>${label}</button>
      </oc-tab-item-v1>
    `)}
  ${[tabBarContent1Url, tabBarContent2Url, tabBarContent3Url, tabBarContent4Url, tabBarContent5Url].map(url => html`
      <oc-tab-panel-v1 slot="panel" url="${url}">
        <div style="display: flex; justify-content: space-between; margin-bottom: 11px;">
          <oc-skeleton-v1 border-radius="8px" width="114px" height="13px"></oc-skeleton-v1>
          <oc-skeleton-v1 border-radius="8px" width="93px" height="13px"></oc-skeleton-v1>
        </div>

        <div style="display: flex; justify-content: space-between; margin-bottom: 24px">
          <oc-skeleton-v1 border-radius="8px" width="64px" height="13px"></oc-skeleton-v1>
          <oc-skeleton-v1 border-radius="8px" width="143px" height="13px"></oc-skeleton-v1>
        </div>
      </oc-tab-panel-v1>
    `)}
</oc-tab-bar-v1>
Story source (TypeScript, verbatim from Storybook)
{
  name: "Demo: Mein Konto with Async Fragments",
  parameters: {
    controls: {
      disabled: true
    },
    chromatic: {
      hideInChromatic: true
    }
  },
  argTypes: hideControlsBadge(Metadata),
  render() {
    return html`
      <p>
        This demonstrates tab bar with tab panels rendering dynamically loaded extrenal html
        fragments (aka async fragments). Each tab panel fetches its content from a separate URL when
        the tab is selected.
      </p>
      <p>
        Each Tab Panel can display a loading state while the content is being fetched, which may be
        usefull for devices with slow network connections. In this example, the content is fetched
        from a local server and may take a few seconds to load.
      </p>
      <p>
        You can test this behavior by ussing developer tools to throttle the network speed to "Slow
        3G" and then clicking on the tabs to see the loading state before the content is displayed.
      </p>
      <p>
        By default the conten will stay in the dom event when the tab has been deselected. You can
        change this behavior by setting the <code>discard-on-inactive</code> attribute on the tab
        panel which should discard the content when the tab is deselected. The tab panel will then
        restore loading state and repeat the fetch when the tab is selected again.
      </p>
      <hr />
      <oc-tab-bar-v1>
        ${[["person", "Mein Konto"], ["order", "Bestellungen"], ["euro", "Rechnungen"], ["otto-up", "Mein UP"], ["chat", "Nachrichten"], ["settings", "Profil"]].map(([icon, label]) => html`
            <oc-tab-item-v1>
              <button><oc-icon-v1 type="${icon}"></oc-icon-v1>${label}</button>
            </oc-tab-item-v1>
          `)}
        ${[tabBarContent1Url, tabBarContent2Url, tabBarContent3Url, tabBarContent4Url, tabBarContent5Url].map(url => html`
            <oc-tab-panel-v1 slot="panel" url="${url}">
              <div style="display: flex; justify-content: space-between; margin-bottom: 11px;">
                <oc-skeleton-v1 border-radius="8px" width="114px" height="13px"></oc-skeleton-v1>
                <oc-skeleton-v1 border-radius="8px" width="93px" height="13px"></oc-skeleton-v1>
              </div>

              <div style="display: flex; justify-content: space-between; margin-bottom: 24px">
                <oc-skeleton-v1 border-radius="8px" width="64px" height="13px"></oc-skeleton-v1>
                <oc-skeleton-v1 border-radius="8px" width="143px" height="13px"></oc-skeleton-v1>
              </div>
            </oc-tab-panel-v1>
          `)}
      </oc-tab-bar-v1>
    `;
  }
}

Interaction tests (TabBarV1.interactions.stories.ts)

Interaction test stories (automated tests, not usage patterns).

Keyboard Navigation

Story: components-tab-bar-interaction-tests--keyboard-navigation · tags: components, tab-bar, 1, interactions, play-fn

Test keyboard navigation with arrow keys

Args: defaultSlot=(see snippet)

Story source (TypeScript, verbatim from Storybook)
{
  args: {
    defaultSlot: `
      <oc-tab-item-v1>
        <button data-testid="tab-1">Tab 1</button>
      </oc-tab-item-v1>
      <oc-tab-item-v1>
        <button data-testid="tab-2">Tab 2</button>
      </oc-tab-item-v1>
      <oc-tab-item-v1>
        <button data-testid="tab-3">Tab 3</button>
      </oc-tab-item-v1>
      <oc-tab-panel-v1 slot="panel">Panel 1</oc-tab-panel-v1>
      <oc-tab-panel-v1 slot="panel">Panel 2</oc-tab-panel-v1>
      <oc-tab-panel-v1 slot="panel">Panel 3</oc-tab-panel-v1>
    `
  },
  play: async ({
    canvasElement
  }) => {
    const canvas = within(canvasElement);
    const tab1 = canvas.getByTestId("tab-1");
    const tab2 = canvas.getByTestId("tab-2");

    // Wait for component to initialize
    await waitFor("tabbar to initialize", async () => {
      // First tab should be selected initially
      await expect(tab1.getAttribute("aria-selected"), "tab1 aria-selected").toBe("true");
      await expect(tab2.getAttribute("aria-selected"), "tab2 aria-selected").toBe("false");
    });

    // Focus first tab and navigate with arrow key
    tab1.focus();
    await macrotasks();
    await userEvent.keyboard("{ArrowRight}");
    await waitFor("tab 2 to be selected", async () => {
      await expect(tab1.getAttribute("aria-selected"), "tab1 aria-selected").toBe("false");
      await expect(tab2.getAttribute("aria-selected"), "tab2 aria-selected").toBe("true");
    });
  }
}

Disabled Tab Handling

Story: components-tab-bar-interaction-tests--disabled-tab-handling · tags: components, tab-bar, 1, interactions, play-fn

Test that disabled tabs cannot be selected

Args: defaultSlot=(see snippet)

Story source (TypeScript, verbatim from Storybook)
{
  args: {
    defaultSlot: `
      <oc-tab-item-v1>
        <button data-testid="tab-1">Tab 1</button>
      </oc-tab-item-v1>
      <oc-tab-item-v1 disabled>
        <button data-testid="tab-2">Disabled Tab</button>
      </oc-tab-item-v1>
      <oc-tab-item-v1>
        <button data-testid="tab-3">Tab 3</button>
      </oc-tab-item-v1>
      <oc-tab-panel-v1 slot="panel">Panel 1</oc-tab-panel-v1>
      <oc-tab-panel-v1 slot="panel">Panel 2</oc-tab-panel-v1>
      <oc-tab-panel-v1 slot="panel">Panel 3</oc-tab-panel-v1>
    `
  },
  play: async ({
    canvasElement
  }) => {
    const canvas = within(canvasElement);
    const tab2 = canvas.getByTestId("tab-2");
    waitFor("Disabled tab should have aria-disabled", async () => {
      await expect(tab2.getAttribute("aria-disabled")).toBe("true");
    });

    // Clicking disabled tab should not select it
    await userEvent.click(tab2);
    waitFor("Disabled tab should have aria-selected", async () => {
      await expect(tab2.getAttribute("aria-selected")).not.toBe("true");
    });
  }
}

Aria Attributes

Story: components-tab-bar-interaction-tests--aria-attributes · tags: components, tab-bar, 1, interactions, play-fn

Test ARIA attributes are correctly set

Args: defaultSlot=(see snippet)

Story source (TypeScript, verbatim from Storybook)
{
  args: {
    defaultSlot: `
      <oc-tab-item-v1>
        <button data-testid="tab-1">Tab 1</button>
      </oc-tab-item-v1>
      <oc-tab-item-v1>
        <button data-testid="tab-2">Tab 2</button>
      </oc-tab-item-v1>
      <oc-tab-panel-v1 data-testid="panel-1" slot="panel">Panel 1</oc-tab-panel-v1>
      <oc-tab-panel-v1 data-testid="panel-2" slot="panel">Panel 2</oc-tab-panel-v1>
    `
  },
  play: async ({
    canvasElement
  }) => {
    const canvas = within(canvasElement);
    await new Promise(resolve => setTimeout(resolve, 100));
    const tab1 = canvas.getByTestId("tab-1");
    const panel1 = canvas.getByTestId("panel-1");

    // Tab should have role and aria-controls
    await expect(tab1.getAttribute("role"), "tab1 role").toBe("tab");
    await expect(tab1.hasAttribute("aria-controls"), "tab1 has attribute aria-controls").toBe(true);

    // Panel should have role and aria-labelledby
    await expect(panel1.getAttribute("role"), "panel1 role").toBe("tabpanel");
    await expect(panel1.hasAttribute("aria-labelledby"), "panel1 has attribute aria-labelledby").toBe(true);

    // Verify tab controls the panel
    const panelId = tab1.getAttribute("aria-controls");
    await expect(panel1.id, "panel1 id").toBe(panelId);
  }
}