Skip to content

Tabs <l-tabs>

Tabs organize content into panels, showing one at a time. Progressive enhancement — plain HTML is enhanced with ARIA roles, keyboard navigation, and an animated indicator. Mark the initially-active tab with aria-selected="true" so it is styled as selected before the script loads.

HTML tag<l-tabs>
Native
Progressive
Plain
Shadow-DOM
Progressive Custom Element

Options

Enclosed variant

Add variant="enclosed" for a pill-shaped tablist with a sliding background indicator.

Make changes to your account here.
Change your password here.
Manage your notification preferences.
Code
html
<l-tabs variant="enclosed">
  <div>
    <button
      name="account"
      aria-selected="true"
    >
      Account
    </button>
    <button name="password">Password</button>
    <button name="notifications">Notifications</button>
  </div>
  <div class="p-4 text-sm text-secondary">Make changes to your account here.</div>
  <div class="p-4 text-sm text-secondary">Change your password here.</div>
  <div class="p-4 text-sm text-secondary">Manage your notification preferences.</div>
</l-tabs>

Line variant

Add variant="line" for a tablist with a sliding underline indicator.

Make changes to your account here.
Change your password here.
Manage your notification preferences.
Code
html
<l-tabs variant="line">
  <div>
    <button
      name="account"
      aria-selected="true"
    >
      Account
    </button>
    <button name="password">Password</button>
    <button name="notifications">Notifications</button>
  </div>
  <div class="p-4 text-sm text-secondary">Make changes to your account here.</div>
  <div class="p-4 text-sm text-secondary">Change your password here.</div>
  <div class="p-4 text-sm text-secondary">Manage your notification preferences.</div>
</l-tabs>

Hovering a tab reveals a subtle pill behind its label, detached from the bottom border. Restyle it with --hover-color (and --hover-inset for the gap), along with the active underline and the static bottom border via --indicator-color, --indicator-thickness, --track-color and --track-thickness.

High-level overview.
Traffic and engagement analytics.
Exported reports.
Code
html
<l-tabs
  variant="line"
  style="
    --indicator-color: var(--l-color-bg-fill-info-strong);
    --indicator-thickness: 3px;
    --track-color: var(--l-color-divider);
    --hover-color: var(--l-color-bg-fill-info-soft);
  "
>
  <div>
    <button
      name="overview"
      aria-selected="true"
    >
      Overview
    </button>
    <button name="analytics">Analytics</button>
    <button name="reports">Reports</button>
  </div>
  <div class="p-4 text-sm text-secondary">High-level overview.</div>
  <div class="p-4 text-sm text-secondary">Traffic and engagement analytics.</div>
  <div class="p-4 text-sm text-secondary">Exported reports.</div>
</l-tabs>

Full width

Add full-width to stretch tabs across the container.

Make changes to your account here.
Change your password here.
Manage your notification preferences.
Code
html
<l-tabs
  variant="enclosed"
  full-width
>
  <div>
    <button
      name="account"
      aria-selected="true"
    >
      Account
    </button>
    <button name="password">Password</button>
    <button name="notifications">Notifications</button>
  </div>
  <div class="p-4 text-sm text-secondary">Make changes to your account here.</div>
  <div class="p-4 text-sm text-secondary">Change your password here.</div>
  <div class="p-4 text-sm text-secondary">Manage your notification preferences.</div>
</l-tabs>

Default active tab

Set value="1" to activate a specific tab on load (0-based index).

Make changes to your account here.
Change your password here.
Manage your notification preferences.
Code
html
<l-tabs
  variant="enclosed"
  value="1"
>
  <div>
    <button name="account">Account</button>
    <button
      name="password"
      aria-selected="true"
    >
      Password
    </button>
    <button name="notifications">Notifications</button>
  </div>
  <div class="p-4 text-sm text-secondary">Make changes to your account here.</div>
  <div class="p-4 text-sm text-secondary">Change your password here.</div>
  <div class="p-4 text-sm text-secondary">Manage your notification preferences.</div>
</l-tabs>

Disabled tab

Add disabled to a tab button to take it out of play. It is skipped by arrow keys, Home and End, it cannot be selected by a click or by value, and it never shows the hover pill. aria-disabled="true" behaves identically — reach for it only when the native attribute is out of reach (a framework binding, a non-button element).

l-tabs itself takes no disabled attribute. To freeze tab switching — while a page loads, say — put inert on the tablist div: the tabs leave the tab sequence and stop answering clicks, and the active panel stays reachable.

Make changes to your account here.
Change your password here.
Upgrade your plan to manage billing.
Manage your notification preferences.
Code
html
<l-tabs variant="line">
  <div>
    <button
      name="account"
      aria-selected="true"
    >
      Account
    </button>
    <button name="password">Password</button>
    <button
      name="billing"
      disabled
    >
      Billing
    </button>
    <button name="notifications">Notifications</button>
  </div>
  <div class="p-4 text-sm text-secondary">Make changes to your account here.</div>
  <div class="p-4 text-sm text-secondary">Change your password here.</div>
  <div class="p-4 text-sm text-secondary">Upgrade your plan to manage billing.</div>
  <div class="p-4 text-sm text-secondary">Manage your notification preferences.</div>
</l-tabs>

Accessibility

Criteria

Role

First child div gets role="tablist", buttons get role="tab", remaining divs get role="tabpanel"

WCAG4.1.2
RGAA7.1
Linked controls

Each tab has aria-controls pointing to its panel; each panel has aria-labelledby pointing back to its tab

WCAG1.3.1
Selection state

Active tab has aria-selected="true"; inactive tabs have aria-selected="false"

WCAG4.1.2
Focus management

Roving tabindex — active tab has tabindex="0", others tabindex="-1". Panels have tabindex="0" for keyboard access

WCAG2.1.1
RGAA12.13
Disabled tabs

Tabs marked disabled or aria-disabled="true" are skipped by arrow keys, Home and End, and never take the roving tabindex="0" — so the tablist always keeps a reachable entry point

WCAG2.1.1
RGAA7.3
Hidden content

Inactive panels use the hidden attribute

WCAG1.3.2
Motion

Indicator animation respects prefers-reduced-motion

WCAG2.3.3

Keyboard interactions

ArrowRight
Moves focus to the next enabled tab and activates it (horizontal orientation)
ArrowLeft
Moves focus to the previous enabled tab and activates it (horizontal orientation)
ArrowDown
Moves focus to the next enabled tab and activates it (vertical orientation)
ArrowUp
Moves focus to the previous enabled tab and activates it (vertical orientation)
Home
Moves focus to the first enabled tab and activates it
End
Moves focus to the last enabled tab and activates it
Tab
Moves focus out of the tablist to the active panel

API reference

Importing

js
import 'luxen-ui/tabs';
css
@import 'luxen-ui/css/tabs/enclosed';
css
@import 'luxen-ui/css/tabs/line';

Attributes & Properties

variantTabsVariantdefault:'line'Property
Visual variant.
valuestringdefault:'0'Property
Index of the active tab (0-based).
full-widthbooleandefault:falseProperty
Stretch tabs to fill container width.
orientationTabsOrientationdefault:'horizontal'Property
Tab orientation.

Events

changeEvent
Fired when the active tab changes. Bubbles. Not cancelable. Properties: index: number, name: string | null.

CSS custom properties

--indicator-colordefault:var(--l-color-text-primary)Custom property
line variant: color of the active underline that slides under the selected tab.
--indicator-thicknessdefault:2pxCustom property
line variant: thickness of the active underline.
--track-colordefault:var(--l-color-border)Custom property
line variant: color of the static bottom border the tabs sit on.
--track-thicknessdefault:1pxCustom property
line variant: thickness of the static bottom border.
--hover-colordefault:var(--l-color-bg-state-hover)Custom property
line variant: fill of the pill that appears behind a hovered tab label.
--hover-insetdefault:4pxCustom property
line variant: block-axis gap between the hover pill and the tab edges, keeping it detached from the bottom border.