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 panel.
Settings panel
Notifications panel
<div class="tabs underlined" role="tablist">
<input type="radio" name="basic-tabs-html" id="tab-profile" class="tab-input" checked aria-controls="panel-profile">
<label for="tab-profile" class="tab-label" role="tab">Profile</label>
<div id="panel-profile" class="tab-panel" role="tabpanel" aria-labelledby="tab-profile">
Profile panel.
</div>
<input type="radio" name="basic-tabs-html" id="tab-settings" class="tab-input" aria-controls="panel-settings">
<label for="tab-settings" class="tab-label" role="tab">Settings</label>
<div id="panel-settings" class="tab-panel" role="tabpanel" aria-labelledby="tab-settings">
Settings panel
</div>
<input type="radio" name="basic-tabs-html" id="tab-notifications" class="tab-input"
aria-controls="panel-notifications">
<label for="tab-notifications" class="tab-label" role="tab">Notifications</label>
<div id="panel-notifications" class="tab-panel" role="tabpanel" aria-labelledby="tab-notifications">
Notifications panel
</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 container

The container that organizes the tab grid.

Attribute Value Description
class "tabs" Required class for the parent container.
role "tablist" Required for accessibility.
class "underlined" Optional variant class.

Tab State (input)

A hidden radio button that manages the selection state.

Attribute Value Description
type "radio" Required for state management.
class "tab-input" Required for behavior and styling.
name string Shared across all tabs in the group.
checked boolean Applied to the initially active tab.
aria-controls ID Should match the ID of the corresponding panel.

Tab Trigger (label)

Attribute Value Description
class "tab-label" Required for behavior and styling.
for ID Must match the ID of the radio input.
role "tab" Required for accessibility.

Tab Panel (div)

Attribute Value Description
class "tab-panel" Required for behavior and styling.
role "tabpanel" Required for accessibility.
aria-labelledby ID Should match the ID of the corresponding trigger.

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