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.
<l-tabs>Options
Enclosed variant
Add variant="enclosed" for a pill-shaped tablist with a sliding background indicator.
Code
<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.
Code
<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.
Code
<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.
Code
<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).
Code
<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.
Code
<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>Panels with interactive content
Tab moves focus out of the tablist and into the active panel, so the content at the top of the panel is reachable. A panel that opens with something focusable needs no stop of its own: l-tabs leaves its tabindex off and Tab reaches that first control directly, instead of stopping on the panel box first.
Every other panel keeps tabindex="0" — including one that opens with text and holds a button further down. Dropping the stop there would send a keyboard user past everything above that button.
This panel opens with a link, so pressing Tab from the tablist moves focus straight to it.
This panel opens with text, so it takes a tab stop of its own — Tab lands on the panel, then on the link.
Download the summaryCode
<l-tabs variant="line">
<div>
<button
name="report"
aria-selected="true"
>
Report
</button>
<button name="summary">Summary</button>
</div>
<div class="p-4 text-sm text-secondary">
<a
href="#"
class="font-semibold text-primary underline-offset-4 hover:underline"
>Download the report</a
>
<p class="mt-2">
This panel opens with a link, so pressing Tab from the tablist moves focus straight to it.
</p>
</div>
<div class="p-4 text-sm text-secondary">
<p>
This panel opens with text, so it takes a tab stop of its own — Tab lands on the panel, then
on the link.
</p>
<a
href="#"
class="mt-2 inline-block font-semibold text-primary underline-offset-4 hover:underline"
>Download the summary</a
>
</div>
</l-tabs>The check runs again whenever a panel's content changes, so a panel filled after mount — a framework rendering into it once its data lands — is treated the same as one present in the initial markup. A tabindex you write on a panel yourself is left alone.
Accessibility
Criteria
- Role
First child div gets
role="tablist", buttons getrole="tab", remaining divs getrole="tabpanel"WCAG4.1.2RGAA7.1- Linked controls
Each tab has
aria-controlspointing to its panel; each panel hasaria-labelledbypointing back to its tabWCAG1.3.1- Selection state
Active tab has
aria-selected="true"; inactive tabs havearia-selected="false"WCAG4.1.2- Focus management
Roving tabindex — active tab has
tabindex="0", otherstabindex="-1". A panel takestabindex="0"unless its first content is already focusable, per the APGWCAG2.1.1RGAA12.13- Disabled tabs
Tabs marked
disabledoraria-disabled="true"are skipped by arrow keys,HomeandEnd, and never take the rovingtabindex="0"— so the tablist always keeps a reachable entry pointWCAG2.1.1RGAA7.3- Hidden content
Inactive panels use the
hiddenattributeWCAG1.3.2- Motion
Indicator animation respects
prefers-reduced-motionWCAG2.3.3
Rules
- The first child of
l-tabsmust be adivcontainingbuttonelements for the tab triggers - Each remaining child
divmaps to a tab panel in order - Do not add
roleoraria-*attributes manually — the custom element sets them automatically l-tabshas no host-leveldisabledattribute — putinerton the tablistdivto freeze tab switching
Keyboard interactions
API reference
Importing
import 'luxen-ui/tabs';@import 'luxen-ui/css/tabs/enclosed';@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 propertylinevariant: color of the active underline that slides under the selected tab.--indicator-thicknessdefault:2pxCustom propertylinevariant: thickness of the active underline.--track-colordefault:var(--l-color-border)Custom propertylinevariant: color of the static bottom border the tabs sit on.--track-thicknessdefault:1pxCustom propertylinevariant: thickness of the static bottom border.--hover-colordefault:var(--l-color-bg-state-hover)Custom propertylinevariant: fill of the pill that appears behind a hovered tab label.--hover-insetdefault:4pxCustom propertylinevariant: block-axis gap between the hover pill and the tab edges, keeping it detached from the bottom border.