Skip to content

WorkspaceTabs

<caos-workspace-tabs>

the app-scoped tab row, with the open records beneath it

May be placed on a seam in the workspace frame.

Name Attribute Description Type Default Required
tabs property only The active app’s destinations, in order, as { id, label, href, active?, object?, caret?, icon?, overflow? }. object marks it as an object tab — its label is set uppercase and the key rides the caret event. caret draws the per-tab chevron. icon names a glyph from the shared vocabulary; a name outside it draws no glyph rather than a blank box. overflow moves the tab out of the bar and into the trailing “More” menu — with none marked, the bar is exactly the plain bar. json none declared Optional
homeTabs property only Destinations PINNED into the row, as { id, label, href, icon?, active? }, drawn between the anchor and the app’s tabs. Unlike tabs they survive an app switch, which is the whole of what they promise — so there is no overflow here (a tab that hides itself when the bar is tight is not permanently visible) and no object/caret (these are destinations, not windows onto one object). Choosing one raises the same workspace-navigate an app tab raises. json none declared Optional
activeAppLabel active-app-label, markup only The name of the app that is open, drawn after the launcher in the display face so it reads as the title of the row. Unset ⇒ nothing is drawn there. text none declared Optional
homeLabel home-label, markup only What the leading anchor is called. Unset ⇒ “Home”. A shell whose home IS an app passes that app’s name; the anchor never names a package on its own, because whether that package is installed is the shell’s knowledge and not this component’s. text Home Optional
homeActive home-active, markup only The home surface is the one on screen, so the leading anchor reads active instead of one of the tabs. boolean false Optional
Name When it fires What it carries
workspace-home The leading anchor was chosen. Go home; do NOT change which app is active. nothing
workspace-navigate A tab was chosen — a pinned one in the bar, or one from the “More” menu. Both raise this, so an overflow tab activates exactly like any other. { href, id } — where to go, and which tab said so.
tab-caret An object tab’s chevron was pressed. The row opens nothing; it says which object and where the chevron is. { id, object, anchor: { left, bottom } } — the caret’s live screen rectangle, ready to hand to ObjectCaret’s openAt.
Name What goes in it
launcher The App Launcher, at the head of the row. It stays pinned while the tab set scrolls past it.
gadget The Customize Navigation control, at the far end. Also pinned.
Design token What it controls
--caos-color-surface The band itself — the DATA surface, distinct from the page base the bar above it paints.
--caos-color-border The rule under the row, the divider after the app name, and the “More” menu’s outline.
--caos-color-accent The underline under the active tab, and the dot beside the active item in the “More” menu.
--caos-color-bg The fill behind the active tab, and behind a “More” menu row on hover.
--caos-color-text The app name, the active tab, and a tab under the pointer.
--caos-color-text-muted A resting tab label and a resting caret.
--caos-focus-ring The ring on a tab, a caret, the “More” button or a menu row reached by keyboard.
--caos-font-display The typeface the current app’s name is set in.
--caos-font-mono The typeface the tab labels and the “More” menu are set in.
--caos-tracking-tight The letter-spacing of the current app’s name.
--caos-radius-sm The corner radius of a focus ring, and of a “More” menu row.
--caos-radius The “More” menu’s corner radius.
--caos-shadow-float The shadow under the “More” menu.
--caos-space-1 The padding inside the “More” menu.
--caos-space-2 The space either side of the divider, and the inset at the far end before the gadget.
--caos-space-3 The gap between the launcher and the current app’s name.

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; the home anchor and the overflow control carry it too Always

Only as the workspace frame’s app-scoped row. The tabs are already scoped by the active app, so switching apps swaps the whole set; nothing here is a general-purpose tab control.

  • TabStrip — The choices are facets of ONE record or one configuration object, inside a page.
  • OpenRecords — The row is the records somebody has open rather than the app’s destinations.
  • AppLauncher — Somebody is moving to a different app, not to a destination inside this one.
  • SegmentedFilter — The row changes what a list is showing rather than moving anywhere.

The anchor and the tabs are a real role="tablist" with a roving tabindex — only the active tab is in the tab order, and Arrow Left/Right, Home and End move between them; the trailing “More” button joins the same roving set so the collapsed tabs stay reachable. The launcher sits OUTSIDE the tablist, because it is a menu button and not a tab. The “More” menu is a role="menu" that takes focus when it opens, roams with Up/Down, jumps with Home/End, closes on Escape returning focus to its button, and marks the active item aria-current; each per-tab caret is a button named “ list views” and is out of the tab order, since the row is reached by arrow key. What is left to the author: the row labels itself “Workspace tabs” unless given an aria-label, and a caret’s aria-expanded belongs to whoever opens the popover it reports.

The row as an app hands it over: the home anchor, then the app’s objects with their glyphs and their per-tab carets, one of them active.

The page around it: The surrounding page is the shell: it answers a caret press by filling in that object’s recents and opening the popover at the rectangle the row reported.

{
"id": "example",
"section": "An app’s object tabs",
"columns": 1,
"items": [
{
"id": "workspace_tabs_1",
"type": "component",
"key": "workspace_tabs",
"inputs": {
"home_label": "Atlantis AI",
"active_app_label": "Sales Atlas",
"tabs": [
{
"id": "o:account",
"label": "Accounts",
"href": "/sales-atlas/account",
"object": "account",
"icon": "building-2",
"caret": true,
"active": true
},
{
"id": "o:contact",
"label": "Contacts",
"href": "/sales-atlas/contact",
"object": "contact",
"icon": "users",
"caret": true
},
{
"id": "o:invoice",
"label": "Invoices",
"href": "/sales-atlas/invoice",
"object": "invoice",
"icon": "file-text",
"caret": true
},
{
"id": "reports",
"label": "Reports",
"href": "/sales-atlas/reports",
"icon": "table-2"
}
]
},
"children": [
{
"id": "object_caret_2",
"type": "component",
"key": "object_caret",
"inputs": {
"views": [
{
"id": "v1",
"label": "All Accounts",
"href": "/sales-atlas/account?view=all"
},
{
"id": "v2",
"label": "My Accounts",
"href": "/sales-atlas/account?view=mine"
}
]
}
}
]
}
]
}
<caos-workspace-tabs id="workspaceTabs1" home-label="Atlantis AI" active-app-label="Sales Atlas">
<caos-object-caret id="workspaceTabs1objectCaret1"></caos-object-caret>
</caos-workspace-tabs>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const workspaceTabs1 = document.getElementById('workspaceTabs1');
const workspaceTabs1objectCaret1 = document.getElementById('workspaceTabs1objectCaret1');
workspaceTabs1.tabs = [
{
"id": "o:account",
"label": "Accounts",
"href": "/sales-atlas/account",
"object": "account",
"icon": "building-2",
"caret": true,
"active": true
},
{
"id": "o:contact",
"label": "Contacts",
"href": "/sales-atlas/contact",
"object": "contact",
"icon": "users",
"caret": true
},
{
"id": "o:invoice",
"label": "Invoices",
"href": "/sales-atlas/invoice",
"object": "invoice",
"icon": "file-text",
"caret": true
},
{
"id": "reports",
"label": "Reports",
"href": "/sales-atlas/reports",
"icon": "table-2"
}
];
workspaceTabs1objectCaret1.views = [
{
"id": "v1",
"label": "All Accounts",
"href": "/sales-atlas/account?view=all"
},
{
"id": "v2",
"label": "My Accounts",
"href": "/sales-atlas/account?view=mine"
}
];

Both seams filled: the App Launcher pinned at the head and the Customize Navigation control pinned at the far end, with the tab set scrolling between them.

{
"id": "example",
"section": "The row as the shell builds it",
"columns": 1,
"items": [
{
"id": "workspace_tabs_1",
"type": "component",
"key": "workspace_tabs",
"inputs": {
"home_label": "Atlantis AI",
"active_app_label": "Sales Atlas",
"home_active": true,
"tabs": [
{
"id": "o:account",
"label": "Accounts",
"href": "/sales-atlas/account",
"object": "account",
"icon": "building-2"
},
{
"id": "o:invoice",
"label": "Invoices",
"href": "/sales-atlas/invoice",
"object": "invoice",
"icon": "file-text"
}
]
},
"children": [
{
"id": "app_launcher_2",
"type": "component",
"key": "app_launcher",
"inputs": {
"active_app_key": "sales-atlas",
"apps": [
{
"key": "sales-atlas",
"label": "Sales Atlas",
"icon": "building-2",
"destinationCount": 2
},
{
"key": "setup",
"label": "Setup",
"icon": "settings",
"destinationCount": 12
}
]
},
"slot": "launcher"
},
{
"id": "nav_gadget_3",
"type": "component",
"key": "nav_gadget",
"inputs": {
"app_label": "Sales Atlas",
"items": [
{
"id": "account",
"label": "Accounts",
"href": "/sales-atlas/account"
},
{
"id": "invoice",
"label": "Invoices",
"href": "/sales-atlas/invoice"
}
],
"available": [
{
"id": "order",
"label": "Orders",
"href": "/sales-atlas/order"
}
]
},
"slot": "gadget"
}
]
}
]
}
<caos-workspace-tabs id="workspaceTabs1" home-label="Atlantis AI" active-app-label="Sales Atlas" home-active>
<caos-app-launcher id="workspaceTabs1appLauncher1" slot="launcher" active-app-key="sales-atlas"></caos-app-launcher>
<caos-nav-gadget id="workspaceTabs1navGadget2" slot="gadget" app-label="Sales Atlas"></caos-nav-gadget>
</caos-workspace-tabs>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const workspaceTabs1 = document.getElementById('workspaceTabs1');
const workspaceTabs1appLauncher1 = document.getElementById('workspaceTabs1appLauncher1');
const workspaceTabs1navGadget2 = document.getElementById('workspaceTabs1navGadget2');
workspaceTabs1.tabs = [
{
"id": "o:account",
"label": "Accounts",
"href": "/sales-atlas/account",
"object": "account",
"icon": "building-2"
},
{
"id": "o:invoice",
"label": "Invoices",
"href": "/sales-atlas/invoice",
"object": "invoice",
"icon": "file-text"
}
];
workspaceTabs1appLauncher1.apps = [
{
"key": "sales-atlas",
"label": "Sales Atlas",
"icon": "building-2",
"destinationCount": 2
},
{
"key": "setup",
"label": "Setup",
"icon": "settings",
"destinationCount": 12
}
];
workspaceTabs1navGadget2.items = [
{
"id": "account",
"label": "Accounts",
"href": "/sales-atlas/account"
},
{
"id": "invoice",
"label": "Invoices",
"href": "/sales-atlas/invoice"
}
];
workspaceTabs1navGadget2.available = [
{
"id": "order",
"label": "Orders",
"href": "/sales-atlas/order"
}
];

An app with more destinations than the bar should carry: the ones marked for overflow collapse into a trailing “More” menu, and the active surface living inside it still underlines the button.

{
"id": "example",
"section": "A long tab set",
"columns": 1,
"items": [
{
"id": "workspace_tabs_1",
"type": "component",
"key": "workspace_tabs",
"inputs": {
"home_label": "Atlantis AI",
"active_app_label": "Sales Atlas",
"tabs": [
{
"id": "o:account",
"label": "Accounts",
"href": "/sales-atlas/account",
"object": "account",
"icon": "building-2"
},
{
"id": "o:contact",
"label": "Contacts",
"href": "/sales-atlas/contact",
"object": "contact",
"icon": "users"
},
{
"id": "o:invoice",
"label": "Invoices",
"href": "/sales-atlas/invoice",
"object": "invoice",
"icon": "file-text"
},
{
"id": "o:order",
"label": "Orders",
"href": "/sales-atlas/order",
"object": "order",
"icon": "file-text",
"overflow": true,
"active": true
},
{
"id": "reports",
"label": "Reports",
"href": "/sales-atlas/reports",
"icon": "table-2",
"overflow": true
}
]
}
}
]
}
<caos-workspace-tabs id="workspaceTabs1" home-label="Atlantis AI" active-app-label="Sales Atlas"></caos-workspace-tabs>
// What an attribute cannot hold, set as a property — the same values the preview is drawn with.
const workspaceTabs1 = document.getElementById('workspaceTabs1');
workspaceTabs1.tabs = [
{
"id": "o:account",
"label": "Accounts",
"href": "/sales-atlas/account",
"object": "account",
"icon": "building-2"
},
{
"id": "o:contact",
"label": "Contacts",
"href": "/sales-atlas/contact",
"object": "contact",
"icon": "users"
},
{
"id": "o:invoice",
"label": "Invoices",
"href": "/sales-atlas/invoice",
"object": "invoice",
"icon": "file-text"
},
{
"id": "o:order",
"label": "Orders",
"href": "/sales-atlas/order",
"object": "order",
"icon": "file-text",
"overflow": true,
"active": true
},
{
"id": "reports",
"label": "Reports",
"href": "/sales-atlas/reports",
"icon": "table-2",
"overflow": true
}
];