Components
Tabs
The Tabs are radio inputs and the Panels are just divs that show and hide
based on the radio inputs' :checked state.
Full support Supported since v123. Full support Supported since v120. Full support Supported since v17.5.
Basics
Profile settings and information.
General account settings.
Manage your notifications.
---import { Tabs } from "@opui/astro"---
<Tabs class="underlined"> <Tabs.Item open> <Tabs.Tab>Profile</Tabs.Tab> <Tabs.Panel>Profile settings and information.</Tabs.Panel> </Tabs.Item> <Tabs.Item> <Tabs.Tab>Settings</Tabs.Tab> <Tabs.Panel>General account settings.</Tabs.Panel> </Tabs.Item> <Tabs.Item> <Tabs.Tab>Notifications</Tabs.Tab> <Tabs.Panel>Manage your notifications.</Tabs.Panel> </Tabs.Item></Tabs><div class="tabs underlined" role="tablist"> <input aria-controls="panel-1" checked class="tab-input" id="tab-1" name="tabs-1" type="radio" /> <label for="tab-1" class="tab-label" role="tab"> Profile </label> <div id="panel-1" class="tab-panel" role="tabpanel" aria-labelledby="tab-1"> Profile settings and information. </div> <input aria-controls="panel-2" class="tab-input" id="tab-2" name="tabs-1" type="radio" /> <label for="tab-2" class="tab-label" role="tab"> Settings </label> <div id="panel-2" class="tab-panel" role="tabpanel" aria-labelledby="tab-2"> General account settings. </div> <input aria-controls="panel-3" class="tab-input" id="tab-3" name="tabs-1" type="radio" /> <label for="tab-3" class="tab-label" role="tab"> Notifications </label> <div id="panel-3" class="tab-panel" role="tabpanel" aria-labelledby="tab-3"> Manage your notifications. </div></div>Accessibility
The tab system uses standard radio inputs and labels, so we get group management and keyboard support for free!
Tab List
| Element | Attribute | Description |
|---|---|---|
.tabs | role="tablist" | Identifies the element as a container for a set of tabs. |
input | name | Groups the radio buttons together for exclusive selection. |
label | role="tab" | Identifies the element as a tab to assistive technology. |
Tab Panel
The content area associated with a tab:
| Attribute | Value | Description |
|---|---|---|
role | "tabpanel" | Identifies the element as a tab panel. |
aria-labelledby | string | Links the panel to its trigger ID. |
Keyboard Interaction
- Tab: Moves focus to the active tab trigger (the radio button). Pressing Tab again moves focus out of the tab list to the next focusable element.
- Right Arrow / Down Arrow: Moves focus to the next tab and activates it.
- Left Arrow / Up Arrow: Moves focus to the previous tab and activates it.
API
Tabs
The main container for the tab items.
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Optional class name (e.g., "underlined"). |
name | string | "tabs-XXXX" | A unique name for the exclusive group. Shared with all children. |
Tabs.Item
A logical grouping for a tab trigger and its content. Handles state synchronization.
| Prop | Type | Default | Description |
|---|---|---|---|
open | boolean | false | Whether this tab is initially active. |
Tabs.Tab
The visible trigger for the tab.
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Optional class name. |
Tabs.Panel
The content area for the tab.
| Prop | Type | Default | Description |
|---|---|---|---|
class | string | - | Optional class name. |
Browser support
Full support Supported since v123. Full support Supported since v120. Full support Supported since v17.5.
See also the full browser support guide.
Installation
@layer components.root { :where(.tabs) { --_accent-color: var(--primary); --_bg-color: transparent;
align-items: flex-start; display: flex; flex-wrap: wrap; gap: 0 var(--size-1); position: relative;
&>.tab-input[type="radio"] { /* utils > .sr-only */ /* TODO: use mixin for this when available */ block-size: 1px; clip-path: inset(50%); inline-size: 1px; overflow: hidden; position: absolute; white-space: nowrap;
&:checked {
/* Checked Tab */ &+[role="tab"] { border-block-end-color: var(--_accent-color); color: var(--_accent-color);
/* Checked Panel */ &+[role="tabpanel"] { display: block; } } }
&:focus-visible { &+[role="tab"] { --focus-ring-color: var(--text-muted); --focus-ring-offset: calc(-1 * var(--size-2)); outline: var(--focus-ring-width) var(--focus-ring-style) var(--focus-ring-color); outline-offset: var(--focus-ring-offset); } } }
/* Tab */ &>[role="tab"] { align-items: center; background-color: transparent; border-bottom: 2px solid transparent; border-radius: 0; color: var(--text-muted); cursor: pointer; display: inline-flex; font-weight: 600; justify-content: center; line-height: var(--font-lineheight-4); order: 1; padding: var(--size-2) var(--size-3); position: relative; transition: color 0.1s; user-select: none;
&:hover { background-color: light-dark(oklch(from var(--_accent-color) calc(l * 0.75) none h / 5%), oklch(from var(--_accent-color) calc(l * 1.25) none h / 5%)); } }
/* Tab Panel */ &>[role="tabpanel"] { display: none; inline-size: 100%; order: 2; } }}