A web component that transforms heading-structured content into an accessible tabbed interface. This is a modern web component port of Aaron Gustafson's original TabInterface.
.d.ts files describe both TabbedInterfaceElement and defineTabbedInterface, so editors, bundlers, and framework toolchains get type information automatically.showHeaders, tablistAfter, autoActivate, and defaultTab properties reflect to attributes, keeping declarative markup and imperative code in sync.upgradeProperty helper replays any property assignments that happen before the browser upgrades the element, which is especially helpful for SSR and hydration workflows.defaultTab property accepts either zero-based indices or heading IDs, making it easy to drive tab selection from reactive state or router parameters.npm install @aarongustafson/tabbed-interface
<tabbed-interface>
<h2>First Tab</h2>
<p>Content for the first tab panel.</p>
<h2>Second Tab</h2>
<p>Content for the second tab panel.</p>
<h2>Third Tab</h2>
<p>Content for the third tab panel.</p>
</tabbed-interface>
<script type="module">
import '@aarongustafson/tabbed-interface/define.js';
</script>
Auto-define (browser environments only):
import '@aarongustafson/tabbed-interface/define.js';
// Registers <tabbed-interface> when customElements is available
Prefer to control when registration happens? Call the helper directly:
import { defineTabbedInterface } from '@aarongustafson/tabbed-interface/define.js';
defineTabbedInterface();
Manual registration:
import { TabbedInterfaceElement } from '@aarongustafson/tabbed-interface';
customElements.define('my-tabs', TabbedInterfaceElement);
| Attribute | Type | Default | Description |
|---|---|---|---|
show-headers |
boolean | false |
When present, shows headings in tab panels |
tablist-after |
boolean | false |
When present, positions tab list after content |
default-tab |
string | "0" |
Initial active tab (index or heading ID) |
auto-activate |
boolean | false |
When present, tabs activate on focus; when absent, use Enter/Space to activate |
<!-- Show headings in panels -->
<tabbed-interface show-headers>
...
</tabbed-interface>
<!-- Tabs after content -->
<tabbed-interface tablist-after>
...
</tabbed-interface>
<!-- Start on specific tab -->
<tabbed-interface default-tab="2">
...
</tabbed-interface>
<!-- Start on tab by heading ID -->
<tabbed-interface default-tab="features">
<h2 id="intro">Introduction</h2>
<p>...</p>
<h2 id="features">Features</h2>
<p>...</p>
</tabbed-interface>
<!-- Auto-activation (tabs activate on focus) -->
<tabbed-interface auto-activate>
...
</tabbed-interface>
| Property | Type | Description |
|---|---|---|
activeIndex |
number | Get/set the currently active tab index |
showHeaders |
boolean | Get/set header visibility |
tablistAfter |
boolean | Get/set tablist position |
autoActivate |
boolean | Get/set auto-activation behavior |
| Method | Description |
|---|---|
next() |
Navigate to the next tab |
previous() |
Navigate to the previous tab |
first() |
Navigate to the first tab |
last() |
Navigate to the last tab |
const $tabs = document.querySelector('tabbed-interface');
// Navigate
$tabs.next();
$tabs.previous();
$tabs.first();
$tabs.last();
// Set active tab directly
$tabs.activeIndex = 2;
| Event | Detail | Description |
|---|---|---|
tabbed-interface:change |
{ tabId, tabpanelId, tabIndex } |
Fired when active tab changes |
document.querySelector('tabbed-interface')
.addEventListener('tabbed-interface:change', (e) => {
console.log(`Switched to tab ${e.detail.tabIndex}`);
});
| Key | Action |
|---|---|
Arrow Left/Up |
Previous tab |
Arrow Right/Down |
Next tab |
Home |
First tab |
End |
Last tab |
Enter/Space |
Activate tab (when auto-activate is absent) and focus first focusable element in panel |
Style the component's shadow DOM elements using CSS ::part() selectors:
| Part | Description |
|---|---|
tablist |
The container for all tabs |
tab |
Individual tab buttons |
tabpanel |
Individual tab panel containers |
Basic styling:
tabbed-interface::part(tablist) {
gap: 4px;
background: #f0f0f0;
padding: 8px;
}
tabbed-interface::part(tab) {
padding: 0.75em 1.5em;
background: white;
border: 1px solid #ccc;
border-radius: 4px 4px 0 0;
font-weight: 500;
}
tabbed-interface::part(tab):hover {
background: #e9e9e9;
}
tabbed-interface::part(tabpanel) {
padding: 2em;
border: 1px solid #ccc;
background: white;
}
Targeting specific states:
/* Active tab - use attribute selector on the host */
tabbed-interface::part(tab selected) {
background: white;
border-bottom-color: white;
font-weight: bold;
}
/* Focus styles */
tabbed-interface::part(tab):focus-visible {
outline: 3px solid blue;
outline-offset: 2px;
}
Themed variations:
/* Pills style */
.pills::part(tablist) {
gap: 8px;
background: transparent;
}
.pills::part(tab) {
border-radius: 20px;
background: #e0e0e0;
}
.pills::part(tab)[aria-selected="true"] {
background: #007bff;
color: white;
}
/* Minimal style */
.minimal::part(tab) {
border: none;
border-bottom: 2px solid transparent;
border-radius: 0;
background: transparent;
}
.minimal::part(tab)[aria-selected="true"] {
border-bottom-color: #007bff;
}
.minimal::part(tabpanel) {
border: none;
padding-top: 1.5em;
}
Use data-tab-short-name to show a different label in the tab than the heading. The full heading text is set as the aria-label for screen readers:
<tabbed-interface>
<h2 data-tab-short-name="Intro">Introduction and Getting Started Guide</h2>
<p>Full content with the complete heading visible in the panel.</p>
</tabbed-interface>
The component supports URL hash navigation. Link to specific tabs:
<a href="#features">Go to Features</a>
<tabbed-interface>
<h2 id="intro">Introduction</h2>
<p>...</p>
<h2 id="features">Features</h2>
<p>...</p>
</tabbed-interface>
Works in all modern browsers supporting:
# Install dependencies
npm install
# Run tests
npm test
# Run tests once
npm run test:run
# Lint
npm run lint
# Format code
npm run format
MIT - See LICENSE
Based on the jQuery TabInterface plugin by Aaron Gustafson, which is itself a port of his original TabInterface.