HyTabGroup
Usage examples on this page are written for Lit / plain HTML (
<hy-tab-group>). The same component ships asHyTabGroupin@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
<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
<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
<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
<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
<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
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:
before-change— cancelable event fired when the user (click or keyboard) attempts to switch tabs. Callevent.preventDefault()to block the switch while a fetch is in flight. The event does NOT fire for programmatic updates (settingactivePaneldirectly), matching the HTMLchangeevent pattern — this prevents infinite loops when the consumer's handler eventually commits the switch.loading— whentrue, overlays the panel area with anhy-spinnerand setsaria-busy="true"on the panels container. The tab rail stays interactive so the user can still switch away or cancel.--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
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
orientation | orientation | 'horizontal' | 'vertical-left' | 'vertical-right' | 'horizontal' | The orientation of the tab group. |
activePanel | active-panel | string | '' | The name of the initially active tab panel. |
activation | activation | 'automatic' | 'manual' | 'automatic' | Controls whether tabs are activated automatically on focus or manually on click/enter. |
emphasis | emphasis | '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 |
size | size | 'small' | 'medium' | 'large' | 'medium' | Type + padding rung propagated to every child tab. - small — label-md type on row-nav-md padding - medium (default) — body-md type on row-nav-lg padding - large — body-lg type on row-nav-lg padding |
loading | loading | boolean | false | When 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. |
actionsStartLabel | actions-start-label | string | '' | 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. |
actionsEndLabel | actions-end-label | string | '' | Accessible name for the trailing actions region when it auto-promotes to role="toolbar". |
Events
| Event | Detail | Description |
|---|---|---|
change | — | Fired when the active tab changes. Detail: { activePanel: string, previousPanel: string } |
before-change | — | Cancelable. 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
| Slot | Description |
|---|---|
tab | Tab elements that trigger panel changes |
panel | Tab panel elements that display content |
actions-start | View-global controls rendered at the leading edge of the tab-bar, beside the tablist. Auto-promotes to role="toolbar" with 2+ controls. |
actions-end | View-global controls rendered at the trailing edge of the tab-bar. Auto-promotes to role="toolbar" with 2+ controls. |
CSS Parts
| Part | Description |
|---|---|
base | The tab group's base wrapper element |
tab-bar | The row (column in vertical) wrapping the tablist and the action slots |
tab-list | The container for the tab elements |
actions-start | The leading tab-bar actions region |
actions-end | The trailing tab-bar actions region |
panels | The container for the tab panel elements |
loading-overlay | The spinner overlay rendered when loading is true |
CSS Custom Properties
| Property | Description |
|---|---|
--hy-tab-group-gap | Gap between vertical tabs. Default: var(--hy-gap-layout-2xs). |
--hy-tab-group-actions-gap | Gap between the tablist and the action regions, and between controls within a region. Default: var(--hy-gap-layout-sm). |
--hy-tab-panel-min-height | Minimum 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-background | Background of the loading overlay. Default: transparent (spinner floats over dimmed content). |