npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@aarongustafson/tabbed-interface

v3.0.0

Published

A web component that transforms heading-structured content into an accessible tabbed interface

Readme

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

  • 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.ts files describe both TabbedInterfaceElement and defineTabbedInterface, so editors, bundlers, and framework toolchains get type information automatically.
  • The showHeaders, tablistAfter, autoActivate, and defaultTab properties reflect to attributes, keeping declarative markup and imperative code in sync.
  • A private upgradeProperty helper replays any property assignments that happen before the browser upgrades the element, which is especially helpful for SSR and hydration workflows.
  • The defaultTab property 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-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:

  • 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 format

License

MIT - See LICENSE

Credits

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