7.3 Tabs
Horizontal tab bar with pink underline for the active tab. Optional count chip sits right of the label. Tabs scroll horizontally on mobile.
Underline tabs
.tabsOverview
The panel content matches the selected tab. Use Tab+arrow keys to navigate between tabs.
<div class="tabs-wrap">
<div class="tabs" role="tablist">
<button class="tab is-active" role="tab" type="button" id="tab-overview" aria-selected="true" aria-controls="panel-overview" tabindex="0">Overview</button>
<button class="tab" role="tab" type="button" id="tab-conversations" aria-selected="false" aria-controls="panel-conversations" tabindex="-1">Conversations <span class="tab-count">248</span></button>
<button class="tab" role="tab" type="button" id="tab-handoffs" aria-selected="false" aria-controls="panel-handoffs" tabindex="-1">Handoffs <span class="tab-count">14</span></button>
<button class="tab" role="tab" type="button" id="tab-analytics" aria-selected="false" aria-controls="panel-analytics" tabindex="-1">Analytics</button>
<button class="tab" role="tab" type="button" id="tab-settings" aria-selected="false" aria-controls="panel-settings" tabindex="-1">Settings</button>
</div>
<div class="tabs-body" role="tabpanel" id="panel-overview" aria-labelledby="tab-overview" tabindex="0">
<h4 class="tabs-body-title">Overview</h4>
<p class="tabs-body-text">The panel content matches the selected tab. Use Tab+arrow keys to navigate between tabs.</p>
</div>
<div class="tabs-body" role="tabpanel" id="panel-conversations" aria-labelledby="tab-conversations" tabindex="0" hidden></div>
<div class="tabs-body" role="tabpanel" id="panel-handoffs" aria-labelledby="tab-handoffs" tabindex="0" hidden></div>
<div class="tabs-body" role="tabpanel" id="panel-analytics" aria-labelledby="tab-analytics" tabindex="0" hidden></div>
<div class="tabs-body" role="tabpanel" id="panel-settings" aria-labelledby="tab-settings" tabindex="0" hidden></div>
</div>.tabs-wrap { width: 100%; } /* fluid — flows to the column width on wide layouts, internal overflow-x handles narrow */
.tabs {
display: inline-flex; gap: 0;
border-bottom: 1px solid var(--hair);
width: 100%;
overflow-x: auto;
scrollbar-width: none; /* Firefox: hide fat OS scrollbar */
-ms-overflow-style: none; /* legacy Edge / IE */
}
.tabs::-webkit-scrollbar { display: none; } /* Chrome / Safari */
.tab {
appearance: none; background: transparent; border: 0;
padding: 12px var(--s-4) 14px;
font: 500 13.5px/1 var(--f-body); color: var(--fg-dim);
cursor: pointer;
position: relative; white-space: nowrap;
transition: color var(--dur-2) var(--ease);
display: inline-flex; align-items: center; gap: var(--s-2);
}
.tab:hover { color: var(--fg); }
.tab.is-active { color: var(--fg); }
.tab.is-active::after {
content: ""; position: absolute; left: 10px; right: 10px; bottom: -1px;
height: 2px; background: var(--accent); border-radius: 2px;
}
.tab-count {
font-family: var(--f-mono); font-size: 11px; color: var(--fg-faint);
padding: 2px 6px; background: var(--bg-sunk); border-radius: var(--r-xs);
}
.tab.is-active .tab-count { color: var(--accent-text); background: var(--accent-soft); }
.tabs-body { padding: var(--s-5) 0 0; }
.tabs-body-title { font: 600 15px/1.3 var(--f-display); margin: 0 0 var(--s-2); }
.tabs-body-text { font: 400 14px/1.55 var(--f-body); color: var(--fg-soft); margin: 0; }import { Tabs } from "@magicblocksai/ui";
<Tabs
defaultValue="overview"
items={[
{ id: "overview", label: "Overview", panel: <Overview /> },
{ id: "conversations", label: "Conversations", count: 248, panel: <Conversations /> },
{ id: "handoffs", label: "Handoffs", count: 14, panel: <Handoffs /> },
{ id: "analytics", label: "Analytics", panel: <Analytics /> },
{ id: "settings", label: "Settings", panel: <Settings /> },
]}
/>7.4 Breadcrumbs
Mono-typed trail with soft chevrons. Current page is ink-filled; everything behind is a soft link.
Breadcrumbs
.bc<nav class="bc" aria-label="Breadcrumb">
<a class="bc-item" href="/">Workspace</a>
<span class="bc-sep" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="m9 6 6 6-6 6"/></svg></span>
<a class="bc-item" href="/agents">Agents</a>
<span class="bc-sep" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="m9 6 6 6-6 6"/></svg></span>
<a class="bc-item" href="/agents/inbound">Inbound-qualify</a>
<span class="bc-sep" aria-hidden="true"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="m9 6 6 6-6 6"/></svg></span>
<span class="bc-current" aria-current="page">System prompt</span>
</nav>.bc {
display: inline-flex; align-items: center; gap: var(--s-2);
font: 500 12.5px/1 var(--f-mono);
color: var(--fg-dim);
}
.bc-item { color: var(--fg-soft); text-decoration: none; }
.bc-item:hover { color: var(--accent-text); }
.bc-sep { color: var(--fg-faint); display: inline-flex; }
.bc-current { color: var(--fg); font-weight: 500; }import { Breadcrumbs } from "@magicblocksai/ui";
<Breadcrumbs
items={[
{ label: "Workspace", href: "/" },
{ label: "Agents", href: "/agents" },
{ label: "Inbound-qualify", href: "/agents/inbound" },
{ label: "System prompt" },
]}
/>7.5 Pagination
Two patterns: numbered pagination for long lists, a compact range indicator for dense UIs. Active page fills pink.
Pagination
.pag · .pag-compact<nav class="pag" role="navigation" aria-label="Pagination">
<button type="button" class="pag-btn pag-arr" aria-label="Previous"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="m15 18l-6-6 6-6"/></svg></button>
<button type="button" class="pag-btn" aria-label="Page 1">1</button>
<button type="button" class="pag-btn is-active" aria-current="page" aria-label="Page 2">2</button>
<button type="button" class="pag-btn" aria-label="Page 3">3</button>
<span class="pag-ellipsis" aria-hidden="true">…</span>
<button type="button" class="pag-btn" aria-label="Page 12">12</button>
<button type="button" class="pag-btn pag-arr" aria-label="Next"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="m9 6 6 6-6 6"/></svg></button>
</nav>
<div class="pag-compact">
<span class="mono small">Showing</span>
<strong class="mono">21–40</strong>
<span class="mono small">of</span>
<strong class="mono">248</strong>
<span class="pag-compact-divider"></span>
<button class="pag-btn">Prev</button>
<button class="pag-btn">Next <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><path d="m9 6 6 6-6 6"/></svg></button>
</div>.pag { display: inline-flex; gap: 4px; align-items: center; }
.pag-btn {
min-width: 32px; height: 32px;
display: inline-flex; align-items: center; justify-content: center;
background: transparent; color: var(--fg-soft);
border: 1px solid transparent;
border-radius: var(--r-sm);
font: 500 13px/1 var(--f-mono);
cursor: pointer;
padding: 0 8px;
gap: 4px;
transition: background var(--dur-2) var(--ease), color var(--dur-2) var(--ease), border-color var(--dur-2) var(--ease);
}
.pag-btn:hover { background: var(--bg-sunk); color: var(--fg); }
.pag-btn.is-active { background: var(--accent); color: var(--paper); }
.pag-arr { border-color: var(--hair); }
.pag-ellipsis { color: var(--fg-faint); padding: 0 4px; font-family: var(--f-mono); }
.pag-compact { display: inline-flex; align-items: center; gap: var(--s-2); font-size: 12.5px; color: var(--fg-soft); flex-wrap: wrap; }
.pag-compact strong { color: var(--fg); font-weight: 600; }
.pag-compact-divider { width: 1px; height: 16px; background: var(--hair); margin: 0 var(--s-2); }import { Pagination } from "@magicblocksai/ui";
import { useState } from "react";
function Demo() {
const [page, setPage] = useState(2);
return (
<>
<Pagination total={12} page={page} onChange={setPage} />
<Pagination total={12} page={page} onChange={setPage} compact />
</>
);
}7.7 Stepper
Vertical progress indicator for flows with clear stages — onboarding, setup, wizards. Done steps fill pink; the active step pulses with a soft ring.
Vertical stepper
.stepper- ✓Connect CRM
- ✓Build your agent
- 3Test conversations
- 4Launch
<ol class="stepper">
<li class="step is-done"><span class="step-dot">✓</span><div><div class="step-name">Connect CRM</div><div class="step-meta">HubSpot · connected</div></div></li>
<li class="step is-done"><span class="step-dot">✓</span><div><div class="step-name">Build your agent</div><div class="step-meta">Tone · channels · guardrails</div></div></li>
<li class="step is-active"><span class="step-dot">3</span><div><div class="step-name">Test conversations</div><div class="step-meta">Simulate 20 inbound leads</div></div></li>
<li class="step"><span class="step-dot">4</span><div><div class="step-name">Launch</div><div class="step-meta">Flip the switch</div></div></li>
</ol>.stepper { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 0; }
.step {
display: grid; grid-template-columns: 32px 1fr;
gap: var(--s-4);
padding: var(--s-3) 0;
position: relative;
}
.step:not(:last-child)::before {
content: ""; position: absolute;
top: 38px; bottom: -6px; left: 15px; width: 2px;
background: var(--hair);
}
.step.is-done::before { background: var(--accent); }
.step-dot {
width: 32px; height: 32px; border-radius: 50%;
background: var(--bg-paper); border: 2px solid var(--hair);
display: inline-flex; align-items: center; justify-content: center;
font: 600 13px/1 var(--f-mono); color: var(--fg-dim);
position: relative; z-index: 1;
}
.step.is-done .step-dot { background: var(--accent); border-color: var(--accent); color: var(--paper); }
.step.is-active .step-dot { border-color: var(--accent); color: var(--accent-text); box-shadow: 0 0 0 4px var(--accent-soft); }
.step-name { font: 600 14.5px/1.3 var(--f-display); color: var(--fg); }
.step.is-done .step-name { color: var(--fg-soft); }
.step-meta { font: 400 12.5px/1.4 var(--f-mono); color: var(--fg-dim); }import { Stepper } from "@magicblocksai/ui";
<Stepper
active={2}
steps={[
{ label: "Connect CRM", description: "HubSpot · connected" },
{ label: "Build your agent", description: "Tone · channels · guardrails" },
{ label: "Test conversations", description: "Simulate 20 inbound leads" },
{ label: "Launch", description: "Flip the switch" },
]}
/>7.8 Skip-to-content link
Keyboard-only link that becomes visible on focus. First tab stop on every page. Jumps sighted keyboard and screen-reader users past repeated nav.
Skip link
.skip-linkPositioned off-screen; slides into the top-left on :focus-visible. Pink background, paper text.
<a href="#main" class="skip-link">Skip to content</a>
<!-- later in the document -->
<main id="main">
<h1>Page</h1>
<!-- page content -->
</main>.skip-link {
position: absolute; top: -100px; left: var(--s-4);
padding: 10px 16px; background: var(--accent); color: var(--paper);
border-radius: var(--r-sm); font: 600 14px/1 var(--f-body);
text-decoration: none; z-index: 1000;
transition: top var(--dur-2) var(--ease);
}
.skip-link:focus-visible { top: var(--s-4); }import { SkipLink } from "@magicblocksai/ui";
// First child of <body>. First tab stop on every page.
<body>
<SkipLink targetId="main">Skip to content</SkipLink>
<header>…</header>
<main id="main">…</main>
</body>7.9 Command-K bar
A global command palette triggered by ⌘K. Centred modal with search, grouped results, and keyboard hints. The fastest path through a dense product surface.
⌘K palette
.cmdkLive-filtered list; arrow keys move selection; Enter executes; Esc closes. Group labels use mono uppercase.
<div class="cmdk">
<div class="cmdk-head">
<input class="cmdk-input" placeholder="Search…" />
<kbd class="cmdk-kbd">⌘K</kbd>
</div>
<div class="cmdk-group">
<div class="cmdk-label">Leads</div>
<button class="cmdk-row is-active">Acme Corp</button>
</div>
</div>.cmdk { width: 520px; max-width: 100%; background: var(--bg-paper);
border: 1px solid var(--hair); border-radius: var(--r-lg);
box-shadow: var(--sh-3); overflow: hidden; }
.cmdk-head { display:flex; align-items:center; gap:var(--s-3);
padding: var(--s-4); border-bottom: 1px solid var(--hair); }
.cmdk-input { flex:1; border:0; background:transparent; font: 400 15px/1 var(--f-body); color: var(--fg); outline:none; }
.cmdk-kbd, .cmdk-row kbd { font: 600 11px/1 var(--f-mono); padding: 3px 6px;
background: var(--bg-sunk); border: 1px solid var(--hair);
border-radius: var(--r-xs); color: var(--fg-soft); }
.cmdk-group { padding: var(--s-2) 0; }
.cmdk-label { font: 500 11px/1 var(--f-mono); text-transform: uppercase;
letter-spacing: 0.08em; color: var(--fg-dim);
padding: var(--s-2) var(--s-4); }
.cmdk-row { display:flex; justify-content:space-between; align-items:center;
width:100%; padding: var(--s-3) var(--s-4); border:0;
background:transparent; font: 400 14px/1 var(--f-body); color: var(--fg);
text-align:left; cursor:pointer; }
.cmdk-row.is-active, .cmdk-row:hover { background: var(--accent-soft); color: var(--accent-text); }
.cmdk-foot { display:flex; gap:var(--s-3); padding: var(--s-3) var(--s-4);
border-top: 1px solid var(--hair); background: var(--bg-sunk);
font: 400 12px/1 var(--f-body); color: var(--fg-dim); }
.skip-link-demo { position: absolute; top: -100px; left: var(--s-4);
padding: 10px 16px; background: var(--accent); color: var(--paper);
border-radius: var(--r-sm); font: 600 14px/1 var(--f-body);
text-decoration: none; z-index: 10; transition: top var(--dur-2) var(--ease); }
.skip-link-demo:focus-visible { top: var(--s-4); }
import { useState } from "react";
import { CommandPalette } from "@magicblocksai/ui";
export default function Demo() {
const [open, setOpen] = useState(true);
return (
<CommandPalette
open={open}
onOpenChange={setOpen}
placeholder="Search leads, pages, commands…"
groups={[
{
id: "leads",
label: "Leads",
items: [
{ id: "acme", title: "Acme Corp — Priya Raman", shortcut: "↵", onSelect: () => {} },
{ id: "globex", title: "Globex — Tomás Ruiz", onSelect: () => {} },
],
},
{
id: "actions",
label: "Actions",
items: [
{ id: "new", title: "New conversation", shortcut: "N", onSelect: () => {} },
{ id: "dash", title: "Open dashboard", shortcut: "D", onSelect: () => {} },
],
},
]}
/>
);
}7.10 Industry bar
A slim taxonomy strip that sits directly below the top nav and above the hero on every top-level page. Answers the buyer's silent first question — is this for me? — in under half a second. Warm-neutral surface by default (with an .is-ink variant) + mono uppercase text so it reads as taxonomy, not marketing copy. Each industry links to its own page; the current industry gets an accent-dot indicator. On mobile the strip scrolls horizontally with a fade-edge hint.
Default
.industry-barEyebrow FOR + 6 industries + interpunct dividers. Current industry dot-marked. Use on the homepage and every Tier 1 use-case / compare page.
<nav class="industry-bar" aria-label="Industry">
<div class="industry-bar-track">
<span class="industry-bar-eyebrow">For</span>
<ul class="industry-bar-list" role="list">
<li class="industry-bar-item">
<a class="industry-bar-link is-current" href="/industries/mortgage" aria-current="page">
<span class="industry-bar-dot" aria-hidden="true"></span>
<span class="industry-bar-label">Mortgage</span>
</a>
</li>
<li class="industry-bar-item">
<span class="industry-bar-sep" aria-hidden="true">·</span>
<a class="industry-bar-link" href="/industries/insurance"><span class="industry-bar-label">Insurance</span></a>
</li>
<!-- Auto · Home Services · Solar · Fintech follow the same <li> pattern -->
</ul>
</div>
</nav>
/* Ships in _shared.css — drop in @magicblocksai/css and use the classes. */
.industry-bar { background: var(--bg-warm); border-bottom: 1px solid var(--hair); }
.industry-bar.is-ink { background: var(--ink); color: var(--warm-3); }
.industry-bar-track { display: flex; align-items: center; gap: var(--s-3);
height: 40px; padding: 0 var(--s-5); overflow-x: auto; }
.industry-bar.is-compact .industry-bar-track { height: 32px; }
.industry-bar-link { font: 500 12px/1 var(--f-mono); text-transform: uppercase;
letter-spacing: 0.06em; color: var(--fg-soft); text-decoration: none;
border-bottom: 1px solid transparent; }
.industry-bar-link:hover,
.industry-bar-link:focus-visible { color: var(--accent); border-bottom-color: var(--accent); }
.industry-bar-link.is-current .industry-bar-dot { /* accent dot before the active label */
width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
/* + .industry-bar-eyebrow / -list / -item / -sep / -label, ink variants, mobile
scroll-fade and focus-ring chrome — all in _shared.css */
import { IndustryBar, type IndustryBarItem } from "@magicblocksai/ui";
const INDUSTRIES: IndustryBarItem[] = [
{ slug: "mortgage", label: "Mortgage" },
{ slug: "insurance", label: "Insurance" },
// …auto, home-services, solar, fintech
];
<IndustryBar industries={INDUSTRIES} currentSlug="mortgage" />
Compact
.industry-bar.is-compactDrops the FOR eyebrow and tightens padding. Use where the strip needs to feel secondary — e.g. inside a documentation chrome or on footer secondary nav.
<!-- Compact + no eyebrow (omit the <span class="industry-bar-eyebrow">) -->
<nav class="industry-bar is-compact" aria-label="Industry">
<div class="industry-bar-track">
<ul class="industry-bar-list" role="list">
<li class="industry-bar-item"><a class="industry-bar-link" href="/industries/mortgage"><span class="industry-bar-label">Mortgage</span></a></li>
<li class="industry-bar-item"><span class="industry-bar-sep" aria-hidden="true">·</span><a class="industry-bar-link" href="/industries/insurance"><span class="industry-bar-label">Insurance</span></a></li>
<!-- …auto, home-services, solar, fintech -->
</ul>
</div>
</nav>
.industry-bar {
width: 100%;
background: var(--bg-warm);
border-bottom: 1px solid var(--hair);
font-family: var(--font-mono, ui-monospace, "JetBrains Mono", "SF Mono", Menlo, Consolas, monospace);
}
.industry-bar.is-ink {
background: var(--ink);
color: var(--warm-3, #F4ECE4);
border-bottom-color: rgba(244, 236, 228, 0.12);
}
.industry-bar-track {
display: flex;
align-items: center;
gap: var(--s-3, 12px);
height: 40px;
padding: 0 var(--s-5, 24px);
max-width: 100%;
overflow-x: auto;
scrollbar-width: none;
-ms-overflow-style: none;
}
.industry-bar-track::-webkit-scrollbar { display: none; }
.industry-bar.is-compact .industry-bar-track { height: 32px; }
.industry-bar-eyebrow {
flex: 0 0 auto;
text-transform: uppercase;
letter-spacing: 0.08em;
font-size: 11px;
font-weight: 500;
color: var(--fg-faint);
}
.industry-bar.is-ink .industry-bar-eyebrow {
color: rgba(244, 236, 228, 0.55);
}
.industry-bar-list {
list-style: none;
margin: 0;
padding: 0;
display: flex;
align-items: center;
gap: var(--s-3, 12px);
flex-wrap: nowrap;
}
.industry-bar-item {
display: inline-flex;
align-items: center;
gap: var(--s-3, 12px);
white-space: nowrap;
}
.industry-bar-sep {
color: var(--fg-faint);
font-size: 12px;
user-select: none;
}
.industry-bar.is-ink .industry-bar-sep {
color: rgba(244, 236, 228, 0.35);
}
.industry-bar-link {
display: inline-flex;
align-items: center;
gap: 6px;
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.06em;
font-size: 12px;
font-weight: 500;
color: var(--fg-soft);
padding: 4px 2px;
border-bottom: 1px solid transparent;
transition: color 120ms ease, border-color 120ms ease;
}
.industry-bar.is-ink .industry-bar-link {
color: var(--warm-3, #F4ECE4);
}
@media (min-width: 1024px) {
.industry-bar-link { font-size: 13px; }
}
.industry-bar-link:hover,
.industry-bar-link:focus-visible {
color: var(--accent);
border-bottom-color: var(--accent);
outline: none;
}
.industry-bar-link:focus-visible {
/* The kit's standard focus-ring chrome — rendered via box-shadow so it
doesn't push the row taller. */
box-shadow: 0 0 0 2px var(--bg-warm), 0 0 0 4px var(--accent);
border-radius: 2px;
}
.industry-bar.is-ink .industry-bar-link:focus-visible {
box-shadow: 0 0 0 2px var(--ink), 0 0 0 4px var(--accent);
}
/* …additional rules trimmed for brevity — see _shared.css */
// compact row · eyebrow hidden for secondary contexts (docs chrome, footers)
<IndustryBar
industries={INDUSTRIES}
compact
eyebrow={false}
/>
7.13 Anatomy of the top nav
- 11. Pill shape
border-radius: var(--r-pill); - 22. Backdrop
backdrop-filter: saturate(140%) blur(10px); - 33. Brand dot
box-shadow: 0 0 0 4px var(--accent-soft); - 44. Active underline
height: 2px; background: var(--accent); - 55. CTA
background: var(--accent); box-shadow: var(--sh-pink);
7.15 Link tabs (cross-route)
Cross-route sibling of <Tabs>. Each tab is a real link; the active state is whatever the consumer's router applies. The kit styles both .tab.active and .tab[aria-current="page"] — RR7’s <NavLink> sets both automatically. Framework-agnostic via the linkAs prop. v1.60.0 (app-team R1).
Cross-route nav
.tabs.tabs-linkTwo tabs with an optional count chip. The chapter demo renders both tabs in the inactive state — the active class is applied at runtime by the consumer’s router.
<nav role="navigation" aria-label="Knowledge sections" class="tabs tabs-link">
<a href="/knowledge/collections" class="tab">Collections<span class="tab-count">12</span></a>
<a href="/knowledge/playbooks" class="tab">Sales playbooks</a>
</nav>
/* No new CSS needed — reuses .tabs + .tab.
* Set .active on the matching tab to light it up.
* RR7 NavLink does this for you automatically. */
.tab.active,
.tab[aria-current="page"] { color: var(--fg); }
// In vanilla shells, set [aria-current="page"] on the matching tab:
function setActiveTab(tabsEl, href) {
tabsEl.querySelectorAll('.tab').forEach(a => {
if (a.getAttribute('href') === href) a.setAttribute('aria-current', 'page');
else a.removeAttribute('aria-current');
});
}
import { LinkTabs } from "@magicblocksai/ui";
// Pass your router's NavLink (RR7, Remix, etc.) as linkAs to get
// automatic active-state styling. NavLink applies `.active` and
// `aria-current="page"` when the URL matches.
export function KnowledgeTabs({ linkAs }: { linkAs: React.ElementType }) {
return (
<LinkTabs
ariaLabel="Knowledge sections"
linkAs={linkAs}
items={[
{ to: "/knowledge/collections", label: "Collections", count: 12 },
{ to: "/knowledge/playbooks", label: "Sales playbooks" },
]}
/>
);
}
7.16 Room tabs (top-of-page)
Top-of-page section tabs for “rooms across an entity surface” (the agent builder’s Overview / Persona / Knowledge / …). Same underline visual as <Tabs>; adds per-tab icon + count chip + a trailing slot for actions. Roving tabindex + arrow-key activation. Optionally sticky. v1.61.0 (app-team R2).
Room tabs
.tabs.tabs-roomsThree rooms with one count chip. The chapter demo renders the first tab in the active state.
<nav role="navigation" aria-label="Agent builder rooms" class="tabs tabs-rooms">
<div role="tablist" class="tabs-rooms-list">
<button role="tab" aria-selected="true" aria-current="page" tabindex="0" class="tab is-active">
<span class="tab-label">Overview</span>
</button>
<button role="tab" aria-selected="false" tabindex="-1" class="tab">
<span class="tab-label">Journey</span><span class="tab-count">4</span>
</button>
</div>
</nav>
/* Reuses .tabs / .tab from chapter 6. Adds:
* .tabs-rooms { display: flex; align-items: center; }
* .tabs-rooms-list { flex: 1; }
* .tabs-rooms-trailing { margin-left: auto; }
* .tabs-rooms-sticky { position: sticky; top: 0; background: var(--bg-paper); }
*/
// Wire arrow-key activation in vanilla — or use the kit's RoomTabs which
// handles roving-tabindex and ←/→/Home/End for you.
import { RoomTabs, OverviewIcon, RouteIcon } from "@magicblocksai/ui";
export function AgentRooms() {
const [current, setCurrent] = React.useState("overview");
return (
<RoomTabs
ariaLabel="Agent builder rooms"
rooms={[
{ id: "overview", label: "Overview", icon: <OverviewIcon /> },
{ id: "journey", label: "Journey", icon: <RouteIcon />, count: 4 },
]}
current={current}
onChange={setCurrent}
/>
);
}