Tabs

Layered panels with one visible at a time. The classes know nothing about any component library. The classless build has a Kobalte layer that binds its state attributes to these same classes.

alpha The class names and states here are stable. The look is not: these styles can change in a minor release, without a major version bump.

ClassDoes what
tabsThe tab list: a row with a bottom rule
tabA tab item. Works on a button, an anchor or a label
tab-activeThe selected tab: darker, bold, underlined
tab-disabledA disabled tab: greyed out and inert
tab-contentA tab panel

Basic

On the class-based build you mark the selected tab and wire the clicks. Kobalte sets data-selected, data-disabled and data-orientation itself but never adds tab-active, so /classless/kobalte binds those attributes to the same classes and its markup needs none of them.

<div>
  <div class="tabs" role="tablist">
    <button class="tab tab-active" role="tab" id="t-profile" aria-controls="p-profile" data-selected>Profile</button>
    <button class="tab" role="tab" id="t-dashboard" aria-controls="p-dashboard">Dashboard</button>
    <button class="tab tab-disabled" role="tab" id="t-billing" aria-controls="p-billing">Billing</button>
  </div>
  <div class="tab-content" role="tabpanel" id="p-profile" aria-labelledby="t-profile">Profile details</div>
  <div class="tab-content" role="tabpanel" id="p-dashboard" aria-labelledby="t-dashboard" hidden>Dashboard details</div>
  <div class="tab-content" role="tabpanel" id="p-billing" aria-labelledby="t-billing" hidden>Upgrade to see billing</div>
</div>

Tabs.Indicator has no class of its own, so it lives in the Kobalte layer as .tab-indicator. When one is present the selected tab drops its own underline, so you never get both.

Importing the Kobalte layer

// instead of "pkg:@sparkstone/css/classless"
@use "pkg:@sparkstone/css/classless/kobalte";

Vertical

data-orientation="vertical" turns the list into a column and moves the indicator to the trailing edge. This is Kobalte-only: the classes have no vertical variant.

<div style="display: flex; gap: 1rem">
  <div role="tablist" data-orientation="vertical">
    <button role="tab" id="v-general" aria-controls="vp-general" data-selected>General</button>
    <button role="tab" id="v-members" aria-controls="vp-members">Members</button>
    <div class="tab-indicator" data-orientation="vertical"></div>
  </div>
  <div role="tabpanel" id="vp-general" aria-labelledby="v-general">General settings</div>
  <div role="tabpanel" id="vp-members" aria-labelledby="v-members" hidden>Member settings</div>
</div>

On the class-based build

There is no Kobalte layer for the class-based build, and it does not need one: put the classes on the Kobalte parts yourself. Both paths apply the same declarations, so the two approaches produce identical styles and can be mixed.

const [tab, setTab] = createSignal("week");

<Tabs value={tab()} onChange={setTab}>
  <Tabs.List class="tabs">
    <Tabs.Trigger
      class="tab"
      classList={{ "tab-active": tab() === "week" }}
      value="week"
    >
      This week
    </Tabs.Trigger>
  </Tabs.List>
</Tabs>