Tabbed Interface Web Component

npm version Build Status

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

TypeScript & Framework Support

Installation

npm install @aarongustafson/tabbed-interface

Usage

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 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);

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
fixed-tabs boolean false When present, keeps tabs active even when the complete tablist does not fit

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>

<!-- Always use tabs; the implementor handles any overflow -->
<tabbed-interface fixed-tabs>
  ...
</tabbed-interface>

Responsive Behavior

The component uses tabs only when the complete, styled tablist fits on one horizontal row. Tab labels may wrap within their buttons. If the tablist does not fit, the component automatically presents the original content as fully expanded linear sections with visible headings.

Fit is based on rendered styles rather than a viewport breakpoint. Fonts, padding, borders, CSS parts, custom properties, selected-tab styling, translated labels, and container resizing all participate in the calculation.

Use fixed-tabs only when you want tabs at every available width and will handle overflow yourself.

The current presentation is reflected as data-layout="tabs" or data-layout="linear" on the component for state-specific styling:

tabbed-interface[data-layout="linear"] {
  margin-block: 2rem;
}

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
fixedTabs boolean Get/set whether tabs remain active when they do not fit

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

Authored panel content

Panel content remains in light DOM and retains its original node identity. Style the content you own with ordinary selectors:

.article tabbed-interface p {
  max-inline-size: 65ch;
}

.article tabbed-interface .callout {
  padding: 1rem;
  background: var(--callout-background);
}

Event listeners, live form values, form ownership, nested custom elements, IDs, and runtime state remain attached to the visible authored nodes.

Component structure

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
selected The currently selected tab
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 */
tabbed-interface::part(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(selected) {
  background: #007bff;
  color: white;
}

/* Minimal style */
.minimal::part(tab) {
  border: none;
  border-bottom: 2px solid transparent;
  border-radius: 0;
  background: transparent;
}

.minimal::part(selected) {
  border-bottom-color: #007bff;
}

.minimal::part(tabpanel) {
  border: none;
  padding-top: 1.5em;
}

The responsive measurement probe receives the same parts and inherited styles as the visible tablist. Keep hover and focus styles metric-stable; normal and selected states are included in fit calculation.

CSS custom properties

CSS custom properties offer convenient theme-level control without replacing parts:

Property Purpose
--tabbed-interface-font-family Component font family
--tabbed-interface-tablist-display Tablist display mode
--tabbed-interface-tablist-gap Gap between tabs
--tabbed-interface-tablist-padding Tablist padding
--tabbed-interface-tablist-margin Tablist margin
--tabbed-interface-tablist-background Tablist background
--tabbed-interface-tablist-border Tablist border
--tabbed-interface-tab-padding Tab padding
--tabbed-interface-tab-background Default tab background
--tabbed-interface-tab-color Default tab text color
--tabbed-interface-tab-border Tab border
--tabbed-interface-tab-border-radius Tab border radius
--tabbed-interface-tab-active-background Selected tab background
--tabbed-interface-tab-active-color Selected tab text color
--tabbed-interface-tab-hover-background Hover/focus background
--tabbed-interface-tab-hover-color Hover/focus text color
--tabbed-interface-tab-focus-outline Keyboard focus outline
--tabbed-interface-tabpanel-padding Panel padding
--tabbed-interface-tabpanel-background Panel background
--tabbed-interface-tabpanel-border Panel border

Printing

Print mode hides the tab controls and reveals every section and original heading in source order. The on-screen selected tab and responsive presentation are restored after printing.

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:

Development

# Install dependencies
npm install

# Run tests
npm test

# Run tests once
npm run test:run

# Lint
npm run lint

# Format code
npm run format

License

MIT - See LICENSE

Credits

Based on the jQuery TabInterface plugin by Aaron Gustafson, which is itself a port of his original TabInterface.