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.
- Progressive Enhancement: Works with semantic HTML structure
- Accessibility: Full ARIA support and keyboard navigation
- Customizable: Extensive CSS custom properties for styling
- Lightweight: No dependencies, pure vanilla JavaScript
- Modern: Uses Shadow DOM and ES Modules
- TypeScript-ready: Ships bundled type definitions for editors and build tooling
- Bundled
.d.tsfiles describe bothTabbedInterfaceElementanddefineTabbedInterface, so editors, bundlers, and framework toolchains get type information automatically. - The
showHeaders,tablistAfter,autoActivate, anddefaultTabproperties reflect to attributes, keeping declarative markup and imperative code in sync. - A private
upgradePropertyhelper replays any property assignments that happen before the browser upgrades the element, which is especially helpful for SSR and hydration workflows. - The
defaultTabproperty 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 availablePrefer 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:
- Custom Elements v1
- Shadow DOM v1
- ES Modules
# Install dependencies
npm install
# Run tests
npm test
# Run tests once
npm run test:run
# Lint
npm run lint
# Format code
npm run formatMIT - See LICENSE
Based on the jQuery TabInterface plugin by Aaron Gustafson, which is itself a port of his original TabInterface.