HyTabPanel
Usage examples on this page are written for Lit / plain HTML (
<hy-tab-panel>). The same component ships asHyTabPanelin@whitespaceux/harmony-react(native React 19), with typed wrappers for Angular, Solid, Svelte, and Vue. The API reference below applies to all frameworks.
Used inside <hy-tab-group> — it only means something as part of it.
A tab panel component that displays content associated with a specific tab. Designed to work within a tab group system and supports lazy loading for performance optimization. Provides proper accessibility attributes and visibility management based on tab selection state.
Usage Guidelines
Tab panels should be used within an hy-tab-group component. Each panel is associated with a specific tab via matching name attributes. Only the active panel is visible at any time.
Lazy Loading
When the lazy attribute is set, the panel projects its content only after it has been activated at least once (and never server-side). This saves layout and paint for tabs with heavy content. The children themselves are ordinary light DOM — they exist, and a <script> among them runs, from parse time — so to fetch data on first activation, listen for the tab group's change event instead.
Accessibility: built in
- Uses
role="tabpanel"for proper ARIA semantics - Sets
aria-hiddenbased on active state - Manages
tabindexfor keyboard navigation - Supports focus management for keyboard users
Examples
Basic usage
<hy-tab-panel name="panel1">
<h2>Panel Content</h2>
<p>This content is displayed when the associated tab is selected.</p>
</hy-tab-panel>Within tab group
<hy-tab-group>
<hy-tab slot="tab" label="Overview" panel="overview-panel"></hy-tab>
<hy-tab slot="tab" label="Details" panel="details-panel"></hy-tab>
<hy-tab-panel slot="panel" name="overview-panel">
<div class="overview-content">
<h3>Project Overview</h3>
<p>Summary of key project metrics and status.</p>
</div>
</hy-tab-panel>
<hy-tab-panel slot="panel" name="details-panel">
<div class="details-content">
<h3>Detailed Information</h3>
<p>Comprehensive project details and specifications.</p>
</div>
</hy-tab-panel>
</hy-tab-group>Lazy loading for performance
<hy-tab-panel name="heavy-content" lazy>
<div class="expensive-content">
<h3>Resource-Intensive Content</h3>
<div class="chart-container">
<!-- Complex charts or heavy DOM content -->
<canvas id="performance-chart"></canvas>
</div>
<div class="data-table">
<!-- Large data tables -->
<table>
<!-- Thousands of rows -->
</table>
</div>
</div>
</hy-tab-panel>Vertical orientation
<hy-tab-group orientation="vertical-left">
<hy-tab slot="tab" label="Dashboard" panel="dashboard"></hy-tab>
<hy-tab slot="tab" label="Settings" panel="settings"></hy-tab>
<hy-tab-panel slot="panel" name="dashboard" orientation="vertical-left">
<div class="dashboard-layout">
<div class="widget-grid">
<div class="widget">
<h4>Active Users</h4>
<div class="metric">1,234</div>
</div>
<div class="widget">
<h4>Revenue</h4>
<div class="metric">$45,678</div>
</div>
</div>
</div>
</hy-tab-panel>
<hy-tab-panel slot="panel" name="settings" orientation="vertical-left">
<div class="settings-form">
<h4>Application Settings</h4>
<form>
<label
>Theme:
<select>
<option>Light</option>
<option>Dark</option>
</select></label
>
<label
>Language:
<select>
<option>English</option>
<option>Spanish</option>
</select></label
>
</form>
</div>
</hy-tab-panel>
</hy-tab-group>Form-based panels
<hy-tab-group>
<hy-tab slot="tab" label="Personal Info" panel="personal"></hy-tab>
<hy-tab slot="tab" label="Contact Details" panel="contact"></hy-tab>
<hy-tab slot="tab" label="Preferences" panel="preferences"></hy-tab>
<hy-tab-panel slot="panel" name="personal">
<form class="personal-form">
<h3>Personal Information</h3>
<div class="form-row">
<label>First Name: <input type="text" required /></label>
<label>Last Name: <input type="text" required /></label>
</div>
<div class="form-row">
<label>Date of Birth: <input type="date" /></label>
<label
>Gender:
<select>
<option>Prefer not to say</option>
</select></label
>
</div>
</form>
</hy-tab-panel>
<hy-tab-panel slot="panel" name="contact">
<form class="contact-form">
<h3>Contact Details</h3>
<div class="form-row">
<label>Email: <input type="email" required /></label>
<label>Phone: <input type="tel" /></label>
</div>
<div class="form-row">
<label>Address: <textarea rows="3"></textarea></label>
</div>
</form>
</hy-tab-panel>
<hy-tab-panel slot="panel" name="preferences" lazy>
<form class="preferences-form">
<h3>User Preferences</h3>
<div class="preference-group">
<h4>Notifications</h4>
<label><input type="checkbox" /> Email notifications</label>
<label><input type="checkbox" /> SMS notifications</label>
<label><input type="checkbox" /> Push notifications</label>
</div>
<div class="preference-group">
<h4>Privacy</h4>
<label><input type="radio" name="profile" /> Public profile</label>
<label><input type="radio" name="profile" /> Private profile</label>
</div>
</form>
</hy-tab-panel>
</hy-tab-group>Dynamic content with lazy loading
<hy-tab-group id="report-tabs">
<hy-tab slot="tab" label="Summary" panel="summary" active></hy-tab>
<hy-tab slot="tab" label="Analytics" panel="analytics"></hy-tab>
<hy-tab-panel slot="panel" name="summary">…</hy-tab-panel>
<hy-tab-panel slot="panel" name="analytics" lazy>
<div id="analytics-content"></div>
</hy-tab-panel>
</hy-tab-group>
<script>
// Fetch once, the first time the Analytics tab is opened.
let loaded = false;
document.getElementById('report-tabs').addEventListener('change', async (event) => {
if (event.detail.activePanel !== 'analytics' || loaded) return;
loaded = true;
const data = await (await fetch('/api/analytics')).json();
document.getElementById('analytics-content').textContent = data.summary;
});
</script>Programmatic Access
const panel = document.querySelector('hy-tab-panel[name="settings"]');
// Check if panel is active
console.log(panel.active);
// Check if panel uses lazy loading
console.log(panel.lazy);
// Get panel name
console.log(panel.name);Header toolbar
The toolbar slot hosts controls scoped to this panel (e.g. a Find + view-switch, or an Edit → Save/Cancel inline-edit set). It renders as the panel's first row, right-aligned, before the content in DOM order so it's announced first. The heading stays consumer-authored in the default slot — compose a Polaris-style header by placing the heading at the top of the content and letting the toolbar float to the trailing edge.
The toolbar area becomes a labeled role="group" when it holds 2 or more controls; name it with toolbar-label. Its controls are reached with Tab — it is not a role="toolbar", which would promise arrow keys between them (slot an hy-toolbar into it for one tab stop with arrow keys). The toolbar wrapper renders only when the slot is populated, so an empty toolbar slot reserves no header space.
Inline-edit toolbar
<hy-tab-panel slot="panel" name="general">
<hy-button slot="toolbar" emphasis="plain">Cancel</hy-button>
<hy-button slot="toolbar" variant="brand">Save</hy-button>
<h2>General</h2>
…
</hy-tab-panel>API
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
name | name | string | '' | The unique identifier for this tab panel, used to match with tab's panel attribute. |
active | active | boolean | false | Marks the tab panel as active/visible. Typically managed by parent tab-group. |
orientation | orientation | 'horizontal' | 'vertical-left' | 'vertical-right' | 'horizontal' | The orientation of the tab panel (inherited from tab-group). |
lazy | lazy | boolean | false | Whether this panel is lazy-loaded (content only renders when first activated). Improves initial page load performance for tabs with heavy content. |
flush | flush | boolean | false | Removes the panel's padding, so its content lines up with the tab strip's edge — for content that draws its own inset (a list, a table, a canvas of its own). |
toolbarLabel | toolbar-label | string | '' | Accessible name applied as aria-label to the toolbar when it becomes a labeled role="group" (2+ controls). Recommended whenever the toolbar carries multiple controls so the toolbar has a name. |
label | label | string | '' | Accessible name for the tabpanel, announced by screen readers. When omitted, the parent hy-tab-group syncs it from the matching tab's label. Forwarded as a string because a plain IDREF aria-labelledby cannot reach role="tab" in the sibling tab's shadow root. |
Slots
| Slot | Description |
|---|---|
default | The content to display when the tab panel is active |
toolbar | Panel-scoped controls rendered as a right-aligned header row above the content. Becomes a labeled role="group" with 2+ controls. |
CSS Parts
| Part | Description |
|---|---|
base | The tab panel's base wrapper element |
toolbar | The panel header toolbar region |
CSS Custom Properties
The documented hooks. Every override hook the component reads — one per token it paints — is listed in @whitespaceux/harmony/theme-api.json, and @whitespaceux/harmony/component-bindings.json (the Harmony MCP get-bindings tool) says which part and state each one paints.
| Property | Default | Description |
|---|---|---|
--hy-tab-panel-padding-layout-2xl | var(--hy-padding-layout-2xl) | Panel padding. |
--hy-tab-panel-padding-layout-none | var(--hy-padding-layout-none) | Panel padding when flush. |
--hy-tab-panel-gap-layout-sm | var(--hy-gap-layout-sm) | Gap between toolbar controls and below the toolbar. |
--hy-tab-panel-background | transparent | Panel background (transparent unless set) |
--hy-tab-panel-min-height | — | Panel minimum height (0 unless set: as tall as its content). Set it, or the group's --hy-tab-group-panel-min-height, to keep panels of different heights from shifting the page. |