Skip to main content

Theme config

Theme mode

Color palette

Grays

Border radii/radiuses/radiopedes/you know
Border radius
Field border radius
Button border radius
All
Components
Guides
API
Recent

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;
}
}
}