Components
Tabs
Switch active panel via tab selection while preserving keyboard navigation semantics.
1. Live Preview
vanilla live preview activates on the client; source HTML keeps a readable preview island and primitive contract.
vanilla tabs preview activates on the client.
2. Installation
Install adapter-specific dependencies first, then complete setup via the installation guide. installation docs
pnpm add @nake-ui/primitives @nake-ui/adapter-vanilla @nake-ui/theme-vanilla
3. Usage
Minimal runnable usage for the current adapter.
import "@nake-ui/theme-vanilla/styles.css";
import { bindTabs } from "@nake-ui/adapter-vanilla";
const binding = bindTabs(nodes, items, options);
binding.destroy();
4. Examples
Line
Set `data-variant="line"` on the tabs list for a line style.
import "@nake-ui/theme-vanilla/styles.css";
import { bindTabs } from "@nake-ui/adapter-vanilla";
if (typeof window !== "undefined" && typeof document !== "undefined") {
list.dataset.variant = "line";
const binding = bindTabs(
{
list,
tabs: { overview: tabOverview, activity: tabActivity, members: tabMembers },
panels: { overview: panelOverview, activity: panelActivity, members: panelMembers }
},
[
{ id: "overview" },
{ id: "activity" },
{ id: "members" }
],
{
id: "vanilla-tabs-line-controller",
defaultValue: "overview"
}
);
// cleanup on unmount
binding.destroy();
}
tabOverview.textContent = "Overview";
Vertical
Use `orientation="vertical"` for vertical tabs.
import "@nake-ui/theme-vanilla/styles.css";
import { bindTabs } from "@nake-ui/adapter-vanilla";
if (typeof window !== "undefined" && typeof document !== "undefined") {
const binding = bindTabs(
{
list,
tabs: { profile: tabProfile, security: tabSecurity, billing: tabBilling },
panels: { profile: panelProfile, security: panelSecurity, billing: panelBilling }
},
[
{ id: "profile" },
{ id: "security" },
{ id: "billing" }
],
{
id: "vanilla-tabs-vertical-controller",
defaultValue: "profile",
orientation: "vertical"
}
);
// cleanup on unmount
binding.destroy();
}
tabProfile.textContent = "Profile";
Disabled
Disabled tabs stay visible but cannot be focused or activated.
import "@nake-ui/theme-vanilla/styles.css";
import { bindTabs } from "@nake-ui/adapter-vanilla";
if (typeof window !== "undefined" && typeof document !== "undefined") {
const binding = bindTabs(
{
list,
tabs: { overview: tabOverview, audit: tabAudit, alerts: tabAlerts },
panels: { overview: panelOverview, audit: panelAudit, alerts: panelAlerts }
},
[
{ id: "overview" },
{ id: "audit", disabled: true },
{ id: "alerts" }
],
{
id: "vanilla-tabs-disabled-controller",
defaultValue: "overview"
}
);
// cleanup on unmount
binding.destroy();
}
tabAudit.textContent = "Audit Logs";
tabAudit.disabled = true;
Icons
Compose icons and labels inside tabs triggers.
import "@nake-ui/theme-vanilla/styles.css";
import { bindTabs } from "@nake-ui/adapter-vanilla";
const tabs = [
{ id: "overview", label: "Overview", icon: "OV" },
{ id: "insights", label: "Insights", icon: "IN" },
{ id: "settings", label: "Settings", icon: "ST" },
];
if (typeof window !== "undefined" && typeof document !== "undefined") {
const binding = bindTabs(
{
list,
tabs: Object.fromEntries(tabs.map((tab) => [tab.id, tabElements[tab.id]])),
panels: Object.fromEntries(tabs.map((tab) => [tab.id, panelElements[tab.id]]))
},
tabs.map((tab) => ({ id: tab.id })),
{
id: "vanilla-tabs-icons-controller",
defaultValue: tabs[0]?.id ?? null
}
);
// cleanup on unmount
binding.destroy();
}
5. RTL
The same structure running inside a `dir="rtl"` container.
vanilla tabs preview activates on the client.
6. Closure Coverage
Complete
Closure Score: 100% (19/19)
Required Checks
-
schemaRegistered -
specDocumented -
reactMapper -
siteExampleCoverage -
siteA11yCoverage -
siteVisible -
schemaValidatorCoverage -
schemaAriaCoverage -
specValidatorRulesSection -
specTestChecklistSection -
specSsrHydrationSection -
reactSsrHydrationCoverage -
reactRenderLevelSsrCoverage -
solidSsrCoverage -
coreImplemented -
primitiveUnitTest -
domContractSnapshot -
vanillaBinding -
adapterParitySnapshot
All required closure checks are currently satisfied.
7. Purpose / Non-goals
Switch active panel via tab selection while preserving keyboard navigation semantics.
- Panel layout/styling
- Async data loading strategy
8. Anatomy & Slot
listtab(repeatable)panel(repeatable)
9. DOM Contract
<div data-ui="tabs" data-slot="list" role="tablist" data-orientation="horizontal">
<button data-slot="tab" role="tab" aria-controls="panel-a" aria-selected="true"></button>
</div>
<div id="panel-a" data-slot="panel" role="tabpanel" aria-labelledby="tab-a"></div>
10. State & Events
State Model
- selected tab id
- controlled:
value+onValueChange - uncontrolled:
defaultValue - activation mode:
manual/automatic
Event Model
- arrow/home/end move roving focus
- Enter/Space activate in manual mode
11. Keyboard & Focus
- roving tabindex required
- exactly one
tabIndex=0tab at a time
12. ARIA
role=tablist/tab/tabpanelaria-selected,aria-controls,aria-labelledby
13. Validator Rules
-
state.multiple-active-roving-item -
focus.invalid-tabindex-roving -
aria.role-mismatch
14. Adapter Notes
Vanilla
- Primary binder: bindTabs.
- Binds explicit DOM nodes and should be cleaned up on unmount.
15. Example Mapping
16. API
Core
-
createTabsControllervalue -
TabsControllertype -
TabsControllerOptionstype -
TabsKeyboardResulttype -
TabsRegistrationtype
Vanilla
-
bindTabsfunction -
TabsElementsinterface