Skip to content

HyTabGroup

Usage examples on this page are written for Lit / plain HTML (<hy-tab-group>). The same component ships as HyTabGroup in @whitespaceux/harmony-react (native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.

A container component that manages a collection of tabs and their associated panels. Provides centralized state management, keyboard navigation, and accessibility features. Coordinates the relationship between tab elements and their corresponding content panels supporting both horizontal and vertical orientations.

Layout Orientations

  • horizontal (default): Tabs displayed in a row above content panels
  • vertical-left: Tabs displayed in a column beside content panels with left indicator
  • vertical-right: Tabs displayed in a column beside content panels with right indicator

Activation Modes

  • automatic (default): Tabs activate immediately when focused with keyboard
  • manual: Tabs require Enter/Space key press to activate after focusing

Keyboard Navigation

  • Arrow Right/Down: Move focus to next tab
  • Arrow Left/Up: Move focus to previous tab
  • Home: Move focus to first tab
  • End: Move focus to last tab
  • Enter/Space: Activate focused tab (manual mode only)

State Management

The tab group automatically:

  • Manages active state across tabs and panels
  • Synchronizes orientation, emphasis and size properties to children
  • Updates tabindex values for proper focus management
  • Skips disabled tabs during keyboard navigation

Examples

Basic usage

html
<hy-tab-group>
  <hy-tab slot="tab" label="Tab 1" panel="panel1"></hy-tab>
  <hy-tab slot="tab" label="Tab 2" panel="panel2"></hy-tab>
  <hy-tab slot="tab" label="Tab 3" panel="panel3"></hy-tab>

  <hy-tab-panel slot="panel" name="panel1">Content for tab 1</hy-tab-panel>
  <hy-tab-panel slot="panel" name="panel2">Content for tab 2</hy-tab-panel>
  <hy-tab-panel slot="panel" name="panel3">Content for tab 3</hy-tab-panel>
</hy-tab-group>

Vertical orientation with icons

html
<hy-tab-group orientation="vertical-left" active-panel="settings">
  <hy-tab slot="tab" label="Dashboard" panel="dashboard">
    <hy-icon slot="icon" name="home"></hy-icon>
  </hy-tab>
  <hy-tab slot="tab" label="Analytics" panel="analytics">
    <hy-icon slot="icon" name="chart"></hy-icon>
  </hy-tab>
  <hy-tab slot="tab" label="Settings" panel="settings">
    <hy-icon slot="icon" name="settings"></hy-icon>
  </hy-tab>

  <hy-tab-panel slot="panel" name="dashboard">
    <h2>Dashboard</h2>
    <p>Overview of your data and metrics.</p>
  </hy-tab-panel>
  <hy-tab-panel slot="panel" name="analytics">
    <h2>Analytics</h2>
    <p>Detailed analytics and reporting.</p>
  </hy-tab-panel>
  <hy-tab-panel slot="panel" name="settings">
    <h2>Settings</h2>
    <p>Configure your application preferences.</p>
  </hy-tab-panel>
</hy-tab-group>

Manual activation mode

html
<hy-tab-group activation="manual">
  <hy-tab slot="tab" label="Profile" panel="profile-panel"></hy-tab>
  <hy-tab slot="tab" label="Security" panel="security-panel"></hy-tab>
  <hy-tab slot="tab" label="Billing" panel="billing-panel"></hy-tab>

  <hy-tab-panel slot="panel" name="profile-panel">
    <h3>Profile Settings</h3>
    <p>Manage your personal information.</p>
  </hy-tab-panel>
  <hy-tab-panel slot="panel" name="security-panel">
    <h3>Security Settings</h3>
    <p>Configure password and authentication settings.</p>
  </hy-tab-panel>
  <hy-tab-panel slot="panel" name="billing-panel">
    <h3>Billing Information</h3>
    <p>View and manage your subscription.</p>
  </hy-tab-panel>
</hy-tab-group>

With disabled tab

html
<hy-tab-group orientation="horizontal" active-panel="overview">
  <hy-tab slot="tab" label="Overview" panel="overview">
    <hy-icon slot="icon" name="eye"></hy-icon>
  </hy-tab>
  <hy-tab slot="tab" label="Projects" panel="projects">
    <hy-icon slot="icon" name="folder"></hy-icon>
  </hy-tab>
  <hy-tab slot="tab" label="Team" panel="team">
    <hy-icon slot="icon" name="users"></hy-icon>
  </hy-tab>
  <hy-tab slot="tab" label="Reports" panel="reports" disabled>
    <hy-icon slot="icon" name="file-text"></hy-icon>
  </hy-tab>

  <hy-tab-panel slot="panel" name="overview">
    <h2>Project Overview</h2>
    <p>View your project statistics and metrics.</p>
  </hy-tab-panel>
  <hy-tab-panel slot="panel" name="projects">
    <h2>Projects</h2>
    <p>Manage your active projects.</p>
  </hy-tab-panel>
  <hy-tab-panel slot="panel" name="team">
    <h2>Team Members</h2>
    <p>View and manage team members.</p>
  </hy-tab-panel>
  <hy-tab-panel slot="panel" name="reports">
    <h2>Reports</h2>
    <p>Feature not yet available.</p>
  </hy-tab-panel>
</hy-tab-group>

Programmatic control

javascript
<hy-tab-group id="responsive-tabs" activation="manual">
<hy-tab slot="tab" label="Mobile" panel="mobile-view"></hy-tab>
<hy-tab slot="tab" label="Tablet" panel="tablet-view"></hy-tab>
<hy-tab slot="tab" label="Desktop" panel="desktop-view"></hy-tab>

<hy-tab-panel slot="panel" name="mobile-view">
<p>Mobile-optimized content and layout.</p>
</hy-tab-panel>
<hy-tab-panel slot="panel" name="tablet-view">
<p>Tablet-optimized content and layout.</p>
</hy-tab-panel>
<hy-tab-panel slot="panel" name="desktop-view">
<p>Desktop-optimized content and layout.</p>
</hy-tab-panel>
</hy-tab-group>

<script>
const tabGroup = document.getElementById('responsive-tabs');

// Set active panel programmatically
tabGroup.activePanel = 'desktop-view';

// Listen for tab changes
tabGroup.addEventListener('change', (event) => {
console.log('Active panel:', event.detail.activePanel);
console.log('Previous panel:', event.detail.previousPanel);
});
</script>

Programmatic Methods

javascript
const tabGroup = document.querySelector('hy-tab-group');

// Change active panel
tabGroup.activePanel = 'settings';

// Change orientation
tabGroup.orientation = 'vertical-left';

// Get current active panel
console.log(tabGroup.activePanel);

Async loading

For tabs whose content is fetched over the network, combine three primitives:

  1. before-change — cancelable event fired when the user (click or keyboard) attempts to switch tabs. Call event.preventDefault() to block the switch while a fetch is in flight. The event does NOT fire for programmatic updates (setting activePanel directly), matching the HTML change event pattern — this prevents infinite loops when the consumer's handler eventually commits the switch.
  2. loading — when true, overlays the panel area with an hy-spinner and sets aria-busy="true" on the panels container. The tab rail stays interactive so the user can still switch away or cancel.
  3. --hy-tab-panel-min-height — CSS custom property that sets a minimum height on the panel container, eliminating layout shifts when switching between panels

### Async panel loading

```html
<hy-tab-group
id="async-tabs"
active-panel="overview"
style="--hy-tab-panel-min-height: 22rem"
>
<hy-tab slot="tab" label="Overview" panel="overview"></hy-tab>
<hy-tab slot="tab" label="Details" panel="details"></hy-tab>

<hy-tab-panel slot="panel" name="overview">…</hy-tab-panel>
<hy-tab-panel slot="panel" name="details" id="details-panel">…</hy-tab-panel>
</hy-tab-group>

<script>
const tabs = document.getElementById('async-tabs');

tabs.addEventListener('before-change', async (event) => {
if (event.detail.activePanel !== 'details') return;
event.preventDefault();              // block the switch while fetching
tabs.loading = true;                 // show spinner overlay
try {
const html = await fetchDetails();
document.getElementById('details-panel').innerHTML = html;
tabs.activePanel = 'details';      // programmatic set does NOT re-fire before-change
} finally {
tabs.loading = false;
}
});
</script>

Tab-bar actions

The actions-start / actions-end slots host controls that apply to the whole tabbed view (e.g. a global search, a "New" primary). They render in the tab-bar as siblings of the role="tablist" element — never inside it — so the tablist stays pure and arrow-key roving is unaffected (the action controls are reached with Tab).

  • actions-start — leading edge (left in horizontal LTR, top of the rail in vertical).
  • actions-end — trailing edge (right in horizontal LTR, bottom of the rail in vertical).
  • Both optional; when empty the tab-bar collapses to the prior layout (zero visual diff).

Each region auto-promotes to role="toolbar" when it holds 2 or more controls (a single control needs no toolbar role per the ARIA APG). Name the toolbar with actions-start-label / actions-end-label. Note: the component manages roving only inside the tablist — within a promoted toolbar the controls are reached with Tab,


### Tab-bar actions

```html
<hy-tab-group active-panel="files">
<hy-tab slot="tab" label="Files" panel="files"></hy-tab>
<hy-tab slot="tab" label="Activity" panel="activity"></hy-tab>
<hy-button slot="actions-end" emphasis="soft">New</hy-button>
<hy-tab-panel slot="panel" name="files">…</hy-tab-panel>
<hy-tab-panel slot="panel" name="activity">…</hy-tab-panel>
</hy-tab-group>

API

Properties

PropertyAttributeTypeDefaultDescription
orientationorientation'horizontal' | 'vertical-left' | 'vertical-right''horizontal'The orientation of the tab group.
activePanelactive-panelstring''The name of the initially active tab panel.
activationactivation'automatic' | 'manual''automatic'Controls whether tabs are activated automatically on focus or manually on click/enter.
emphasisemphasis'solid' | 'soft' | 'outlined' | 'plain''plain'Visual chrome weight propagated to every child tab. - plain (default) — transparent rest, role underline indicator - soft — role-tinted pill on the active tab - outlined — boxed/bordered tabs - solid — fully filled active tab
sizesize'small' | 'medium' | 'large''medium'Type + padding rung propagated to every child tab. - smalllabel-md type on row-nav-md padding - medium (default) — body-md type on row-nav-lg padding - largebody-lg type on row-nav-lg padding
loadingloadingbooleanfalseWhen true, overlays the panel area with a spinner and marks the panels container aria-busy. The tab rail stays interactive so the user can still switch away.
actionsStartLabelactions-start-labelstring''Accessible name applied as aria-label to the leading actions region when it auto-promotes to role="toolbar" (2+ controls). Recommended whenever that region carries multiple controls so the toolbar has a name.
actionsEndLabelactions-end-labelstring''Accessible name for the trailing actions region when it auto-promotes to role="toolbar".

Events

EventDetailDescription
changeFired when the active tab changes. Detail: { activePanel: string, previousPanel: string }
before-changeCancelable. Fired on user-initiated tab switch (click or keyboard) before change. Call event.preventDefault() to block the switch and commit it later via tabGroup.activePanel = target. Not fired for programmatic activePanel updates. Detail: { activePanel: string, previousPanel: string }

Slots

SlotDescription
tabTab elements that trigger panel changes
panelTab panel elements that display content
actions-startView-global controls rendered at the leading edge of the tab-bar, beside the tablist. Auto-promotes to role="toolbar" with 2+ controls.
actions-endView-global controls rendered at the trailing edge of the tab-bar. Auto-promotes to role="toolbar" with 2+ controls.

CSS Parts

PartDescription
baseThe tab group's base wrapper element
tab-barThe row (column in vertical) wrapping the tablist and the action slots
tab-listThe container for the tab elements
actions-startThe leading tab-bar actions region
actions-endThe trailing tab-bar actions region
panelsThe container for the tab panel elements
loading-overlayThe spinner overlay rendered when loading is true

CSS Custom Properties

PropertyDescription
--hy-tab-group-gapGap between vertical tabs. Default: var(--hy-gap-layout-2xs).
--hy-tab-group-actions-gapGap between the tablist and the action regions, and between controls within a region. Default: var(--hy-gap-layout-sm).
--hy-tab-panel-min-heightMinimum height of the panel container. Set this to the expected content height to eliminate layout shifts during async loads. Default: 0.
--hy-tab-group-loading-overlay-backgroundBackground of the loading overlay. Default: transparent (spinner floats over dimmed content).

Built with Lit. Documented with VitePress.