Skip to content

TabStrip

<caos-tab-strip>

horizontal facets of one record or configuration object

May be placed on a record page and a page inside an app.

Name Attribute Description Type Default Required
items property only The tabs, as { value, label, disabled? } objects or as plain strings, in the order they are drawn. A disabled tab is drawn faded, cannot be chosen, and is skipped by the arrow keys. json none declared Optional
value value, also a property Which tab is active, and therefore which panel shows. Reflected as an attribute, so it can be read back off the element. When the page sets none, the first ENABLED tab becomes the value as soon as the tabs are drawn. text none declared Optional
label label, markup only The accessible name of the tab row — what the tabs are facets OF, in the words a screen reader reads out: Record facets, Order facets. Follows the attribute whenever it changes. Without one the row is announced as “Tabs”, which distinguishes nothing on a page carrying two. text none declared Optional
Name When it fires What it carries
change A tab becomes active and was not already — by pointer, or by an arrow key, which moves focus and activates in one gesture. Re-choosing the active tab raises nothing. { value } — the value of the tab now active.
Name What goes in it
panel-<tab value> The panel for the tab whose value this names — panel-details for the tab valued details. Wired up as a tabpanel: labelled by its tab, focusable, shown when its tab is active and hidden when it is not. Optional, and per tab: a tab with no slot filled simply has no panel, and a page may fill some and leave others to be drawn elsewhere. More than one element in one tab’s slot is allowed, and all of them are that tab’s panel.
panel The older way of saying the same thing, still supported and still what hand-written markup usually says: one element per tab in a single shared slot, each carrying data-value matching its tab. A data-value is set on the element rather than declared, so a page assembled from stored metadata cannot write one — which is why the per-tab slots above exist. Elements whose data-value matches no tab are simply left alone.
Design token What it controls
--caos-color-accent The underline under the active tab.
--caos-color-border The rule the tab row sits on.
--caos-color-text The ink of the active tab and of a hovered one.
--caos-color-text-muted The ink of the tabs that are not active.
--caos-font-mono The typeface of the tab labels.
--caos-focus-ring The ring drawn on a keyboard-focused tab.
--caos-space-4 The gap between tabs, and the space above the panel body.

Nothing else drives this piece by calling it.

Nothing on this piece can be read back.

Part Which piece of it When it is there
::part(tablist) the row of tabs Always
::part(tab) one tab Always
::part(panels) the area the panels are slotted into Always

One subject with several faces, only one of which is worth looking at at a time. Reach for it when the alternative is a very long page a person has to scroll to find the part they came for.

  • SegmentedFilter — The row NARROWS A SET rather than swapping the subject — All, Active, Mine on a list of records. That one is announced as pressed buttons rather than as tabs, draws as a filled pill rather than an underline, and belongs on the toolbar of the list it filters.
  • WorkspaceTabs — You are moving between OBJECTS in an app rather than between facets of one record. That is a part of the workspace frame and is not placed on a page at all.
  • DetailSection — Everything should be visible at once and only wants dividing up. Sections stack, so a person can read the whole record and print it; tabs hide what they are not showing.
  • SplitView — A person is moving between several RECORDS while keeping the list in sight, not between facets of one.

A real WAI-ARIA tablist: role="tablist" around role="tab" buttons, aria-selected on the active one, and any slotted panel wired up as role="tabpanel" with aria-labelledby back to its tab and aria-controls forward to the panel — the author puts the panel in that tab’s slot and gets all of that. Keyboard is a roving tabindex: one Tab stop for the whole row, then Arrow Left/Right/Up/Down, Home and End move and activate, skipping disabled tabs. Panels are made focusable so Tab from the row lands in the content. Left to the author: set label saying what the tabs are facets OF — the default name is “Tabs”, which distinguishes nothing on a page with two rows. label is an input rather than an aria-label attribute so that a page assembled from stored metadata can set it; aria-label written straight onto the element is still read, and label wins where both are given. Because arrow keys activate as they move, avoid a tab whose content is expensive to produce.

The tab row on its own, with the surface drawing the body itself and listening for the change.

{
"id": "example",
"section": "Facets of a record",
"columns": 1,
"items": [
{
"id": "tab_strip_1",
"type": "component",
"key": "tab_strip",
"inputs": {
"label": "Record facets",
"value": "details",
"items": [
{
"value": "details",
"label": "Details"
},
{
"value": "related",
"label": "Related"
},
{
"value": "history",
"label": "History"
}
]
}
}
]
}
<caos-tab-strip id="tabStrip1" label="Record facets" value="details"></caos-tab-strip>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const tabStrip1 = document.getElementById('tabStrip1');
tabStrip1.items = [
{
"value": "details",
"label": "Details"
},
{
"value": "related",
"label": "Related"
},
{
"value": "history",
"label": "History"
}
];

The same tabs with slotted panels that really switch — each body sits in its own tab’s slot, and the strip shows one and hides the rest without the surrounding page doing anything.

{
"id": "example",
"section": "Owning its panels",
"columns": 1,
"items": [
{
"id": "tab_strip_1",
"type": "component",
"key": "tab_strip",
"inputs": {
"label": "Order facets",
"value": "details",
"items": [
{
"value": "details",
"label": "Details"
},
{
"value": "related",
"label": "Related"
},
{
"value": "history",
"label": "History"
}
]
},
"children": [
{
"id": "detail_section_2",
"type": "component",
"key": "detail_section",
"inputs": {
"heading": "Order information"
},
"slot": "panel-details",
"children": [
{
"id": "key_value_row_3",
"type": "component",
"key": "key_value_row",
"inputs": {
"label": "Amount",
"value": "$12,400"
}
},
{
"id": "key_value_row_4",
"type": "component",
"key": "key_value_row",
"inputs": {
"label": "Opened",
"value": "14 Mar 2026"
}
}
]
},
{
"id": "detail_section_5",
"type": "component",
"key": "detail_section",
"inputs": {
"heading": "Related records"
},
"slot": "panel-related",
"children": [
{
"id": "key_value_row_6",
"type": "component",
"key": "key_value_row",
"inputs": {
"label": "Account",
"value": "Meridian Supply"
}
},
{
"id": "key_value_row_7",
"type": "component",
"key": "key_value_row",
"inputs": {
"label": "Owner",
"value": "Dana Whitfield"
}
}
]
},
{
"id": "empty_state_8",
"type": "component",
"key": "empty_state",
"inputs": {
"title": "No changes recorded"
},
"slot": "panel-history",
"children": [
"Edits to this record will be listed here."
]
}
]
}
]
}
<caos-tab-strip id="tabStrip1" label="Order facets" value="details">
<caos-detail-section slot="panel-details" heading="Order information">
<caos-key-value-row label="Amount" value="$12,400"></caos-key-value-row>
<caos-key-value-row label="Opened" value="14 Mar 2026"></caos-key-value-row>
</caos-detail-section>
<caos-detail-section slot="panel-related" heading="Related records">
<caos-key-value-row label="Account" value="Meridian Supply"></caos-key-value-row>
<caos-key-value-row label="Owner" value="Dana Whitfield"></caos-key-value-row>
</caos-detail-section>
<caos-empty-state slot="panel-history" title="No changes recorded">Edits to this record will be listed here.</caos-empty-state>
</caos-tab-strip>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const tabStrip1 = document.getElementById('tabStrip1');
tabStrip1.items = [
{
"value": "details",
"label": "Details"
},
{
"value": "related",
"label": "Related"
},
{
"value": "history",
"label": "History"
}
];

One tab disabled — drawn faded, refused to the pointer, and stepped over by the arrow keys rather than focused and then rejected.

{
"id": "example",
"section": "A facet that is not available",
"columns": 1,
"items": [
{
"id": "tab_strip_1",
"type": "component",
"key": "tab_strip",
"inputs": {
"label": "Order facets",
"value": "details",
"items": [
{
"value": "details",
"label": "Details"
},
{
"value": "related",
"label": "Related"
},
{
"value": "approvals",
"label": "Approvals",
"disabled": true
}
]
}
}
]
}
<caos-tab-strip id="tabStrip1" label="Order facets" value="details"></caos-tab-strip>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const tabStrip1 = document.getElementById('tabStrip1');
tabStrip1.items = [
{
"value": "details",
"label": "Details"
},
{
"value": "related",
"label": "Related"
},
{
"value": "approvals",
"label": "Approvals",
"disabled": true
}
];