@aarongustafson/tabbed-interface
v2.1.1
Published
A web component that transforms heading-structured content into an accessible tabbed interface
Maintainers
Readme
Tabbed Interface Web Component
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.
Demo
Features
- 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
TypeScript & Framework Support
- 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.
Installation
npm install @aarongustafson/tabbed-interfaceUsage
Basic Usage
<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>Import Options
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);Attributes
| 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 |
Examples
<!-- 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>Properties
| 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 |
Methods
| 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 |
Programmatic Control
const $tabs = document.querySelector('tabbed-interface');
// Navigate
$tabs.next();
$tabs.previous();
$tabs.first();
$tabs.last();
// Set active tab directly
$tabs.activeIndex = 2;Events
| 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}`);
});Keyboard Navigation
| 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 |
Styling with CSS Parts
Style the component's shadow DOM elements using CSS ::part() selectors:
Available Parts
| Part | Description |
|------|-------------|
| tablist | The container for all tabs |
| tab | Individual tab buttons |
| tabpanel | Individual tab panel containers |
Styling Examples
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;
}Custom Tab Titles
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>Hash Navigation
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>Browser Support
Works in all modern browsers supporting:
- Custom Elements v1
- Shadow DOM v1
- ES Modules
Development
# Install dependencies
npm install
# Run tests
npm test
# Run tests once
npm run test:run
# Lint
npm run lint
# Format code
npm run formatLicense
MIT - See LICENSE
Credits
Based on the jQuery TabInterface plugin by Aaron Gustafson, which is itself a port of his original TabInterface.
