| Version | Tag | Status | API |
|---|---|---|---|
| v1 | <oc-tab-bar-v1> |
Stable, allowed for generation | TabBarV1 |
Overview (v1)
Source: ./src/components/tab-bar/v1/Overview.mdx
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, andoc-tab-panel-v1) are part of the Tab Bar system and live in the same folder.
Default variation
Story Default:
<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
disabledattribute 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 barrole="tab"on each tab item's interactive elementrole="tabpanel"on each panelaria-controlslinking tabs to panelsaria-selectedindicating the current tabaria-labelledbylinking panels to tabstabindexfor roving tab index pattern
Configuration (v1)
Source: ./src/components/tab-bar/v1/Configuration.mdx
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.
Story Default:
<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
Source: ./src/components/tab-bar/v1/TabBarV1.API.g.mdx
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) => voidProgrammatically select a tab by its index.
Parameters:
index:number- The 0-based index of the tab to select
Returns:
voidExample:
const tabBar = document.querySelector('oc-tab-bar-v1'); tabBar.select(2); // Select the third tab
Variations (v1)
Source: ./src/components/tab-bar/v1/Variations.mdx
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)
<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)
<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.).
<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>
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.
<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.
<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>
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.
<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>
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.
<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>
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.
<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>
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.
<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>
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.
<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>
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.
<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>
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.
<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>
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.
<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>
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.
<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>
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.
<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);
}
}