Underline
The classic approach. A quiet border-bottom reveals the active tab while everything else recedes.
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.
Capsule
Pill-shaped tabs nestled inside a recessed track. Feels tactile, works beautifully in toolbars and settings panels.
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.
Outlined
Bordered buttons that fill with color on selection. Bold enough to anchor a hero section or feature comparison.
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.
Ghost
Stripped to the essentials. Serif typography and an animated underline — nothing more.
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.
Animated
CSS animation classes orchestrated by the component. The old panel fades out, then the new one fades in. Try clicking rapidly.
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.
Staggered
List items animate out one by one with a 50ms delay between each, then cascade back in. Rapid clicks cancel cleanly.
Nested
Tab groups inside tab groups. Each instance manages its own state independently — no conflicts, no extra config.
Components
Each component ships as an independent web component. Pick the one below to explore its API.
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
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
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.
Keyboard Navigation
Full keyboard support is built in. Focus a tab and use these keys to navigate.
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;
}