@magic-spells/tab-group

Tabs that get out
of your way

A fully accessible tab group web component with keyboard navigation and ARIA support. Ships only structural CSS — all visual styles are yours to define.

Install npm i @magic-spells/tab-group
Size (gzip) ~1.8 KB
<tab-group>
  <tab-list>
    <tab-button>One</tab-button>
    <tab-button>Two</tab-button>
    <tab-button>Three</tab-button>
  </tab-list>

  <tab-panel>Content for tab one</tab-panel>
  <tab-panel>Content for tab two</tab-panel>
  <tab-panel>Content for tab three</tab-panel>
</tab-group>
Style 01

Underline

The classic approach. A quiet border-bottom reveals the active tab while everything else recedes.

Overview Features Installation

Overview

A zero-dependency web component that registers four custom elements: <tab-group>, <tab-list>, <tab-button>, and <tab-panel>. Drop them into any page and style however you like.

Features

Full ARIA roles and attributes out of the box. Keyboard navigation with arrow keys, Home, and End. A tabchange custom event fires whenever the active tab changes. No Shadow DOM — style the elements directly.

Installation

Install via npm with npm i @magic-spells/tab-group, then import the module. Custom elements register automatically. Include the structural CSS or import it from the package.

Style 02

Capsule

Pill-shaped tabs nestled inside a recessed track. Feels tactile, works beautifully in toolbars and settings panels.

Design Develop Deploy

Design

Start with the markup. The component is unstyled by design, so your creative direction stays intact. Target custom elements and ARIA attributes with plain CSS.

Develop

Import the ES module or drop in the UMD build. Works with every framework — or no framework at all. TypeScript definitions are included if you need them.

Deploy

Under 2 KB gzipped with zero dependencies. No build step required for basic usage. Tree-shaking friendly for bundled projects.

Style 03

Outlined

Bordered buttons that fill with color on selection. Bold enough to anchor a hero section or feature comparison.

Starter Pro Enterprise

Starter

Perfect for personal projects and prototypes. Drop the script tag into your HTML, add the markup, and style with a few lines of CSS. That's it.

Pro

For production apps that need polish. Listen to tabchange events to sync state, lazy-load panel content, or trigger animations when tabs switch.

Enterprise

Meets WCAG 2.1 AA out of the box. The component handles focus management, ARIA labeling, and keyboard navigation so your team doesn't have to.

Style 04

Ghost

Stripped to the essentials. Serif typography and an animated underline — nothing more.

Markup Styling Events

Markup

Nest <tab-button> elements inside a <tab-list>, and place <tab-panel> elements after it. The component pairs them by position automatically.

Styling

Target tab-button[aria-selected="true"] for the active state. Use tab-panel for content areas. Every element is a real DOM node — no shadow boundaries to pierce.

Events

The tabchange event bubbles and carries a detail object with previousIndex, currentIndex, and references to both the tab and panel elements.

Style 05

Animated

CSS animation classes orchestrated by the component. The old panel fades out, then the new one fades in. Try clicking rapidly.

Transition Lifecycle Fallbacks

Transition

Set animate-out-class and animate-in-class attributes on the <tab-group> element. The component adds these classes at the right time and waits for animationend before proceeding.

Lifecycle

ARIA updates and the tabchange event fire immediately. Then the out-class animates the old panel, hidden is swapped, and the in-class animates the new panel. Each attribute works independently.

Fallbacks

If animationend never fires (e.g., no matching @keyframes), the animate-timeout attribute controls a fallback timer. Defaults to 500ms. Rapid clicks cancel cleanly and skip to the target tab.

Style 06

Staggered

List items animate out one by one with a 50ms delay between each, then cascade back in. Rapid clicks cancel cleanly.

Features Stack Principles
01 ARIA RolesAutomatic role, tabindex, and aria-controls
02 Keyboard NavArrow keys, Home, and End support
03 No Shadow DOMStyle elements directly with plain CSS
04 Custom Eventstabchange fires with full detail object
05 CSS AnimationsOrchestrated transitions with class hooks
01 Vanilla JSZero dependencies, pure custom elements
02 RollupESM, CJS, UMD, and minified builds
03 PostCSSStructural CSS processed and minified
04 TypeScriptDeclaration file with full type coverage
05 ESLint + PrettierConsistent code style enforced
01 Zero OpinionsStructural CSS only, visuals are yours
02 ProgressiveWorks without JS, enhances with it
03 ComposableNest tab groups without conflicts
04 AccessibleWCAG 2.1 AA keyboard and screen reader support
05 LightweightUnder 2 KB gzipped with zero deps
Style 07

Nested

Tab groups inside tab groups. Each instance manages its own state independently — no conflicts, no extra config.

Components Utilities Guides

Components

Each component ships as an independent web component. Pick the one below to explore its API.

Tab Group Tab List Tab Button Tab Panel

Tab Group

The root container. Coordinates tabs and panels, assigns ARIA roles, and dispatches tabchange events. Ensures tab/panel count stays in sync automatically.

Tab List

Wraps <tab-button> elements and receives role="tablist". Handles keyboard event delegation for arrow-key navigation between tabs.

Tab Button

Each button receives role="tab", a unique ID, and aria-controls pointing to its panel. Only the active tab has tabindex="0".

Tab Panel

Content containers with role="tabpanel" and aria-labelledby linking back to their tab. Inactive panels receive the hidden attribute.

Utilities

Helper patterns for common scenarios when working with the tab component.

Lazy Loading Deep Linking Animations

Lazy Loading

Listen for tabchange and fetch panel content on demand. The event's detail.currentPanel gives you a direct reference to populate.

Deep Linking

Read the URL hash on page load and call setActiveTab(index) to restore the user's position. Update the hash inside your tabchange handler.

Animations

Add CSS transitions to tab-panel for fade or slide effects. Since panels use the hidden attribute, toggle a class instead for animated transitions.

Guides

Step-by-step walkthroughs for integrating the tab component into your project.

Vanilla JS React Vue

Vanilla JS

Import the module with a <script type="module"> tag. The custom elements register automatically — just write the HTML and add your styles.

React

Import the package in your entry point. Use the custom element names directly in JSX. React 19+ handles custom element properties natively.

Vue

Register the import in your main file and add the tag names to compilerOptions.isCustomElement in your Vue config so the compiler skips them.

Accessibility

Keyboard Navigation

Full keyboard support is built in. Focus a tab and use these keys to navigate.

Tab Focus active tab
Previous tab
Next tab
Home First tab
End Last tab
Quick Start

Bring Your Own Styles

The component ships zero cosmetic CSS. Here's all you need to build a complete tab interface.

tab-list {
  gap: 0.25rem;
  border-bottom: 1px solid #ddd;
}

tab-button {
  padding: 0.5rem 1rem;
  border-bottom: 2px solid transparent;
}

tab-button[aria-selected="true"] {
  color: #3366ff;
  border-bottom-color: #3366ff;
}

tab-panel {
  padding: 1rem;
}