Tabs
Overview
Use the Tabs family when you need a tabbed interface where one content panel is visible at a time, selected by clicking its corresponding trigger. The family is composed, not standalone — the root component manages shared state and keyboard navigation, while child components define the trigger list and content panels. Tabs work well for product feature sections, settings panels, pricing tiers, and any content that benefits from organizing information into mutually exclusive views.
Authoring Structure
Tabs
├── TabsList
│ ├── TabsTrigger (1st)
│ ├── TabsTrigger (2nd)
│ └── TabsTrigger (3rd)
├── TabsContent (1st)
├── TabsContent (2nd)
└── TabsContent (3rd)
Placement Rules
| Component | Placement | Role |
|---|---|---|
| Tabs | Top-level wrapper. | Owns state, keyboard navigation, and shared options. |
| TabsList | Direct child of Tabs. | Container for trigger buttons. Receives role="tablist". |
| TabsTrigger | Inside TabsList. | Interactive button that activates its paired content panel. |
| TabsContent | Direct child of Tabs, sibling to TabsList. | The content panel that shows or hides based on the active trigger. |
Pairing Rule
Triggers and content panels are paired by position — the first TabsTrigger inside TabsList pairs with the first TabsContent that is a direct child of Tabs. The second pairs with the second, and so on. This is purely positional; labels and IDs are not used for matching. If trigger and content counts mismatch, extra triggers are disabled and extra content panels stay hidden.
Quick Start
<OmeTabs settings='{{"orientation":"horizontal"}}'>
{#slot default}
<OmeTabsList>
{#slot default}
<OmeTabsTrigger content='{{"label":"Overview"}}' />
<OmeTabsTrigger content='{{"label":"Details"}}' />
<OmeTabsTrigger content='{{"label":"Changelog"}}' />
{/slot}
</OmeTabsList>
<OmeTabsContent>
{#slot default}<p>Overview content goes here.</p>{/slot}
</OmeTabsContent>
<OmeTabsContent>
{#slot default}<p>Details content goes here.</p>{/slot}
</OmeTabsContent>
<OmeTabsContent>
{#slot default}<p>Changelog content goes here.</p>{/slot}
</OmeTabsContent>
{/slot}
</OmeTabs>
<OmeTabs
settings='{{"orientation":"vertical"}}'
responsiveAccordion='{{"enableResponsiveAccordion":true,"accordionBreakpoint":"768","showAccordionIndicator":true}}'
>
{#slot default}
<OmeTabsList styling='{{"class":"ome-tabs-list-default vertical-nav"}}'>
{#slot default}
<OmeTabsTrigger styling='{{"class":"ome-tabs-trigger-default nav-item"}}'>
{#slot default}Features{/slot}
</OmeTabsTrigger>
<OmeTabsTrigger styling='{{"class":"ome-tabs-trigger-default nav-item"}}'>
{#slot default}Pricing{/slot}
</OmeTabsTrigger>
<OmeTabsTrigger styling='{{"class":"ome-tabs-trigger-default nav-item"}}'>
{#slot default}FAQ{/slot}
</OmeTabsTrigger>
{/slot}
</OmeTabsList>
<OmeTabsContent>
{#slot default}<p>Features content.</p>{/slot}
</OmeTabsContent>
<OmeTabsContent>
{#slot default}<p>Pricing content.</p>{/slot}
</OmeTabsContent>
<OmeTabsContent>
{#slot default}<p>FAQ content.</p>{/slot}
</OmeTabsContent>
{/slot}
</OmeTabs>
Family Components
Tabs (Root)
The root component wraps all lists and panels, and manages shared behavior: active tab state, keyboard navigation, orientation, and optional responsive accordion mode. It does not render any visible UI itself — only a container element.
Structure Props
| Prop | Type | Default | Description |
|---|---|---|---|
tag | string | div | HTML tag for the root element. |
Settings Props
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | horizontal | Controls layout direction and keyboard behavior. |
defaultIndex | string | "0" | Zero-based index of the tab that should be active on initial load. If the index is invalid or points to a disabled trigger, it falls back to the first enabled trigger. |
disabled | boolean | false | Disables the entire tabs interface when |
Styling Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | class | ome-tabs-root-default | CSS class applied to the root element. |
Responsive Accordion Props
| Prop | Type | Default | Description |
|---|---|---|---|
enableResponsiveAccordion | boolean | false | Enables responsive accordion mode. When the viewport width is below |
accordionBreakpoint | string | "768" | Viewport width in pixels below which accordion mode activates. Only applies when |
etchPreviewMode | "auto" | "tabs" | "accordion" | auto | Controls Etch editor preview behavior. |
accordionAnimationDuration | string | "300" | Animation duration in milliseconds for accordion panel expand/collapse. Only applies when responsive accordion is enabled. |
showAccordionIndicator | boolean | false | Adds indicator spans to triggers in accordion mode. Only applies when responsive accordion is enabled. |
accordionItemClass | class | — | CSS class applied to each generated accordion item wrapper. Only applies when responsive accordion is enabled. |
accordionHeaderClass | class | — | CSS class applied to each generated accordion header wrapper. Only applies when responsive accordion is enabled. |
accordionIndicatorClass | class | — | CSS class applied to generated accordion indicators. Only applies when |
Keyboard Behavior — Tabs Mode
When a trigger has focus in tabs mode:
| Key | Action |
|---|---|
Enter / Space | Activate the focused trigger. |
ArrowRight | Move focus to the next trigger and activate it (horizontal). Wraps to first. RTL-aware. |
ArrowLeft | Move focus to the previous trigger and activate it (horizontal). Wraps to last. RTL-aware. |
ArrowDown | Move focus to the next trigger and activate it (vertical). Wraps to first. |
ArrowUp | Move focus to the previous trigger and activate it (vertical). Wraps to last. |
Home | Focus and activate the first enabled trigger. |
End | Focus and activate the last enabled trigger. |
In horizontal tabs, arrow keys automatically reverse in RTL layouts.
Keyboard Behavior — Accordion Mode
When the responsive accordion is active, keyboard behavior changes to match the Accordion component:
| Key | Action |
|---|---|
Enter / Space | Toggle the associated panel. |
ArrowDown | Move focus to the next trigger (does not activate). |
ArrowUp | Move focus to the previous trigger (does not activate). |
Home | Move focus to the first trigger (does not activate). |
End | Move focus to the last trigger (does not activate). |
Focus movement in accordion mode does not auto-activate — you must click or press Enter/Space to open a panel.
Responsive Accordion
When enabled and the viewport drops below accordionBreakpoint, the runtime:
- Creates an accordion list element.
- Moves each trigger-content pair into accordion item wrappers.
- Replaces original positions with comment placeholders.
- Changes ARIA roles (
tab→button,tabpanel→region). - Adds indicators if
showAccordionIndicatoristrue.
When the viewport exceeds the breakpoint, everything is restored to the original tabs DOM.
Accessibility
The root component generates unique IDs for each trigger-content pair and wires up ARIA attributes automatically:
- TabsList:
role="tablist",aria-orientationsynced to theorientationsetting by the runtime. - TabsTrigger:
role="tab",aria-selected,aria-controlspointing to its content panel,tabIndex(0when active,-1when inactive). - TabsContent:
role="tabpanel",aria-labelledbypointing to its trigger,tabIndex="0". - Disabled triggers:
aria-disabled="true",tabIndex="-1". - In accordion mode, triggers use
role="button"andaria-expanded; content panels userole="region".
TabsList
The TabsList is the container for all TabsTrigger elements. It receives role="tablist" and manages focus roving for its child triggers. Place it as a direct child of the root Tabs component.
Styling Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | class | ome-tabs-list-default | CSS class applied to the list element. |
TabsTrigger
The interactive button that activates its paired content panel. When the slot is empty, the trigger renders its content.label value as text. Triggers are paired with content panels by position — the first trigger in TabsList activates the first TabsContent child of Tabs.
Settings Props
| Prop | Type | Default | Description |
|---|---|---|---|
disabled | boolean | false | Disables this trigger independently of the root |
Content Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | "" | Fallback label text. Used when the trigger slot is empty. |
Styling Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | class | ome-tabs-trigger-default | CSS class applied to the trigger element. |
Trigger Slot Content
When the trigger slot is populated, the slot content replaces the fallback label. Use this for rich trigger content — icons, badges, or any markup beyond plain text:
<TabsTrigger>
{#slot default}
<span class="icon">⚙</span> Settings
{/slot}
</TabsTrigger>
TabsContent
The content panel that shows or hides based on the active trigger. Only one panel is visible at a time — Tabs always keeps exactly one panel active. Place content panels as direct children of the root Tabs component, as siblings to TabsList.
Styling Props
| Prop | Type | Default | Description |
|---|---|---|---|
class | class | ome-tabs-content-default | CSS class applied to the content panel. |
CSS Custom Properties
The default tab styles reference these CSS custom properties. Override them to customize appearance without replacing the default classes:
| Variable | Used For | Fallback |
|---|---|---|
--space-s | Gap between list and content panels. | 1rem |
--btn-padding-block | Trigger padding (block axis). | — |
--btn-padding-inline | Trigger padding (inline axis). | — |
--border-color-dark | Inactive trigger border color. | — |
--text-dark-muted | Inactive trigger text color. | — |
--primary | Active trigger border color. | — |
--heading-color | Active trigger text color. | — |
--ome-tabs-accordion-animation-duration | Accordion animation duration (set inline from the accordionAnimationDuration prop). | — |
Pattern Examples
Vertical Tabbed Timeline
Used by the Testimonial8 pattern — a vertical tabbed timeline with custom styled trigger cards:
orientation="vertical"- 4 triggers (Discovery, Onboarding, Building, Scaling) inside custom styled cards
- 4 content panels with testimonials
- Two-column grid layout (
0.35fr / 0.65fr)
Vertical Tabbed Features with Images
Used by the FeatureSection20 pattern — vertical tabs with image content panels:
orientation="vertical"- 4 triggers as feature cards with icons
- 4 content panels showing images
- Two-column grid layout (
0.45fr / 0.55fr)
Both patterns use vertical orientation and custom styling classes to override the default trigger appearance.
Common Mistakes
TabsContent panels must be direct children of the root Tabs component, not inside TabsList. The runtime pairs triggers and content by scanning TabsList children for triggers and Tabs children (excluding TabsList) for content panels. Placing content inside TabsList breaks pairing.
If you have more triggers than content panels, the extra triggers are disabled. If you have more content panels than triggers, the extra panels stay hidden. Always ensure the counts match.
In tabs mode, arrow keys move focus and activate the tab. In accordion mode (when responsive accordion is active), arrow keys only move focus — you must click or press Enter/Space to open a panel.
Triggers and content panels are paired by position only. The second TabsTrigger always pairs with the second TabsContent, regardless of labels or any other attribute. Reordering one without the other breaks the pairing.
FAQs
Can I have nested tabs?
Yes. You can place a Tabs component inside a TabsContent panel. Each nested tabs instance manages its own state independently. Be mindful of the visual complexity — deeply nested tabs can be difficult to navigate.
How does the responsive accordion work?
When enableResponsiveAccordion is true and the viewport drops below accordionBreakpoint, the runtime restructures the DOM into accordion items. Triggers become accordion headers with role="button" and aria-expanded, and content panels become regions. When the viewport exceeds the breakpoint, the original tabs DOM is restored. The transition is seamless — no page reload required.
What if I need all panels collapsed?
Tabs always keeps exactly one panel active. If you need the ability to collapse all panels, use the Accordion component with type="single" instead.
Can I change the default active tab?
Yes. Use the defaultIndex setting on the root Tabs component. It is zero-based — "0" activates the first tab, "1" the second, and so on. If the index is invalid or points to a disabled trigger, it falls back to the first enabled trigger.
How do vertical tabs work with keyboard navigation?
In vertical orientation, ArrowUp and ArrowDown navigate between triggers instead of ArrowLeft and ArrowRight. Home and End still jump to the first and last enabled triggers. The arrow keys auto-activate the focused tab, same as horizontal mode.