Enhancement

Tabs

Tabbed panels built from a list of tab buttons or in-page links

Import
@parallelogram-js/core/components/Tabs
Selector
[data-tabs]
Stylesheet
@parallelogram-js/core/styles/tabs.css

Playground

Without JavaScript the links jump to stacked sections.

About

Without JavaScript the panels are ordinary stacked sections, and links used as tabs jump to them. While scripts are enabled but Tabs hasn't loaded yet, the shipped stylesheet shows only the first panel, so the page doesn't jump when it does. When another tab starts selected, through aria-selected="true" or data-tabs-default-tab, write data-tab-panel="active" on its panel too: the stylesheet shows that panel instead, and Tabs selects it when no tab has aria-selected="true". CSS can't match a tab to its panel, so without the marker the first panel shows until Tabs mounts. If Tabs fails to load, every panel shows again; if it is never registered, the other panels stay hidden.

Once mounted, Tabs follows the WAI-ARIA tabs pattern: arrow keys, Home and End move focus between tabs, inactive panels get the hidden attribute, and a newly chosen panel fades in unless the user prefers reduced motion. Unmounting puts the markup back as it was.

Deep links keep working: when the address names a panel, or an element inside one, Tabs selects that panel's tab when it mounts and whenever the hash changes, ahead of aria-selected and the default tab. In nested tab sets, both the outer and inner tabs holding it are selected. Choosing a tab doesn't change the address.

Usage

Register the component and the framework loads it the first time a page contains [data-tabs].

import { Parallelogram } from '@parallelogram-js/core';

const app = Parallelogram.create();
app.components.add('[data-tabs]', () => import('@parallelogram-js/core/components/Tabs'));
app.run();

The framework is optional. Tabs.enhanceAll() mounts the component on every matching element by itself, for a page that wants one enhancement rather than a framework:

import Tabs from '@parallelogram-js/core/components/Tabs';

Tabs.enhanceAll();

It takes a selector and the component's options, and returns the instance it mounted them with. What it leaves out is what the framework adds: loading a component only on the pages that use it, mounting again after a page swap, and ordering with dependsOn.

Its styles come from the package stylesheet:

@import '@parallelogram-js/core/styles/tabs.css';

Attributes

NameTypeDefaultDescription
data-tabsflagnoneMarks the container
data-tabs-listrequiredflagnoneMarks the tab list. Tabs gives it aria-orientation="horizontal" unless it has one; the Up and Down arrow keys move between tabs only when it is "vertical" On the element holding the tabs.
data-tabstringnoneThe id of the tab's panel On each tab.
data-tabs-panelsrequiredflagnoneMarks the panel container On the element holding the panels.
data-tab-panelactive | entering | inactivenoneMarks a panel; Tabs sets it to the panel's state. Write active on the panel that starts selected to show it before Tabs loads On each panel.
data-tabs-default-tabstringnoneThe id of the panel to show first when the address names no panel and no tab has aria-selected="true" For every instance, Tabs.defaults.defaultTab.
data-tabs-keyboardbooleantrueMove between tabs with the arrow keys, Home and End For every instance, Tabs.defaults.keyboardNavigation.
data-tabs-activationauto | manualautoauto selects a tab when it receives focus; manual selects it on Enter or Space For every instance, Tabs.defaults.activation.
data-tabs-enhancedset by the componentflagnoneWritten once Tabs has seen the container: true when it mounted, false when the markup was missing a tab list, a panel container, or any tabs or panels. Either way the stylesheet stops hiding panels, so markup Tabs cannot use is left showing rather than hidden for good

Events

NameDetailDescription
tabs:change{ activeTab: string; previousTab: string | null; tab: HTMLElement; panel: HTMLElement; timestamp: number }A different tab was selected DOM event and event bus
tabs:mount{ element: HTMLElement; tabCount: number; panelCount: number; timestamp: number }The tabs were set up Event bus

CSS custom properties

NameDefaultDescription
--tabs-transition-duration0.2sLength of the panel fade and tab colour changes
--tabs-colorvar(--color-text-muted, #6b7280)Tab text colour: the muted text colour
--tabs-hover-colorvar(--surface-panel-color-text, #374151)Tab text colour on hover: the panel text colour, which is the page's text colour
--tabs-hover-bgvar(--color-hover, #f9fafb)Tab background on hover: the hover tint
--tabs-selected-colorvar(--color-accent-hover, #0369a1)The chosen tab's text and underline colour: the stronger accent
--tabs-selected-bgtransparentThe chosen tab's background, transparent so its label keeps 4.5:1 contrast on light and grey pages
--tabs-border-colorvar(--surface-panel-border-color, var(--color-border, #e2e8f0))The line under the tab list: the panel border colour