Tabs

A tab set is a row of triggers that switches between content panels. It follows the ARIA tabs pattern, with the <brio-tabs> element handling the keyboard interaction. The styles read their state from ARIA, so the selected, disabled, and focused tabs need no classes, and the shape modes reorient or restyle the tablist.

Examples

Basic usage

Overview

brio is a small CSS component library for internal use, with a JS layer of progressive-enhancement custom elements where the platform doesn't go far enough.

<brio-tabs class="tabs">
                <div role="tablist">
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-selected="true"
                        aria-controls="ex-overview"
                        id="ex-tab-overview"
                    >
                        Overview
                    </button>
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-controls="ex-install"
                        id="ex-tab-install"
                        tabindex="-1"
                    >
                        Install
                    </button>
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-controls="ex-changelog"
                        id="ex-tab-changelog"
                        tabindex="-1"
                    >
                        Changelog
                    </button>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="ex-overview"
                    aria-labelledby="ex-tab-overview"
                >
                    <h3>Overview</h3>
                    <p>
                        brio is a small CSS component library for internal use, with a JS layer of
                        progressive-enhancement custom elements where the platform doesn't go far
                        enough.
                    </p>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="ex-install"
                    aria-labelledby="ex-tab-install"
                    hidden
                >
                    <h3>Install</h3>
                    <p>
                        Clone the repo, run the build, link the bundled CSS and JS. Detailed steps
                        in the README.
                    </p>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="ex-changelog"
                    aria-labelledby="ex-tab-changelog"
                    hidden
                >
                    <h3>Changelog</h3>
                    <p>
                        v0.2 in progress: popover, dropdown, range, tabs, tooltip, plus an extras
                        layer.
                    </p>
                </div>
            </brio-tabs>

Pills

Today's activity, hour by hour.

<brio-tabs class="tabs is-pills">
                <div role="tablist">
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-selected="true"
                        aria-controls="pills-day"
                        id="pills-tab-day"
                    >
                        Day
                    </button>
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-controls="pills-week"
                        id="pills-tab-week"
                        tabindex="-1"
                    >
                        Week
                    </button>
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-controls="pills-month"
                        id="pills-tab-month"
                        tabindex="-1"
                    >
                        Month
                    </button>
                </div>

                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="pills-day"
                    aria-labelledby="pills-tab-day"
                >
                    <p>Today's activity, hour by hour.</p>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="pills-week"
                    aria-labelledby="pills-tab-week"
                    hidden
                >
                    <p>This week's activity, day by day.</p>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="pills-month"
                    aria-labelledby="pills-tab-month"
                    hidden
                >
                    <p>This month's activity, week by week.</p>
                </div>
            </brio-tabs>

Vertical

Profile

Display name, avatar, contact details.

<brio-tabs class="tabs is-vertical">
                <div role="tablist">
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-selected="true"
                        aria-controls="vert-profile"
                        id="vert-tab-profile"
                    >
                        Profile
                    </button>
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-controls="vert-billing"
                        id="vert-tab-billing"
                        tabindex="-1"
                    >
                        Billing
                    </button>
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-controls="vert-team"
                        id="vert-tab-team"
                        tabindex="-1"
                    >
                        Team
                    </button>
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-controls="vert-api"
                        id="vert-tab-api"
                        tabindex="-1"
                    >
                        API keys
                    </button>
                </div>

                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="vert-profile"
                    aria-labelledby="vert-tab-profile"
                >
                    <h3>Profile</h3>
                    <p>Display name, avatar, contact details.</p>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="vert-billing"
                    aria-labelledby="vert-tab-billing"
                    hidden
                >
                    <h3>Billing</h3>
                    <p>Current plan, payment method, invoice history.</p>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="vert-team"
                    aria-labelledby="vert-tab-team"
                    hidden
                >
                    <h3>Team</h3>
                    <p>Members, roles, invitations.</p>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="vert-api"
                    aria-labelledby="vert-tab-api"
                    hidden
                >
                    <h3>API keys</h3>
                    <p>Create, revoke, and rotate access tokens.</p>
                </div>
            </brio-tabs>

Stretched

Pay month to month, cancel anytime.

<brio-tabs class="tabs is-stretched">
                <div role="tablist">
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-selected="true"
                        aria-controls="str-monthly"
                        id="str-tab-monthly"
                    >
                        Monthly
                    </button>
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-controls="str-yearly"
                        id="str-tab-yearly"
                        tabindex="-1"
                    >
                        Yearly (save 20%)
                    </button>
                </div>

                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="str-monthly"
                    aria-labelledby="str-tab-monthly"
                >
                    <p>Pay month to month, cancel anytime.</p>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="str-yearly"
                    aria-labelledby="str-tab-yearly"
                    hidden
                >
                    <p>One yearly payment, two months free.</p>
                </div>
            </brio-tabs>

Activation modes

Manual activation: focus the tablist with Tab, walk between tabs with arrow keys, then press Enter or Space to switch panels.

<brio-tabs class="tabs" manual>
                <div role="tablist">
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-selected="true"
                        aria-controls="man-a"
                        id="man-tab-a"
                    >
                        Tab A
                    </button>
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-controls="man-b"
                        id="man-tab-b"
                        tabindex="-1"
                    >
                        Tab B
                    </button>
                    <button
                        class="tab"
                        type="button"
                        role="tab"
                        aria-controls="man-c"
                        id="man-tab-c"
                        tabindex="-1"
                    >
                        Tab C
                    </button>
                </div>

                <div class="tab-panel" role="tabpanel" id="man-a" aria-labelledby="man-tab-a">
                    <p>
                        Manual activation: focus the tablist with Tab, walk between tabs with arrow
                        keys, then press Enter or Space to switch panels.
                    </p>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="man-b"
                    aria-labelledby="man-tab-b"
                    hidden
                >
                    <p>
                        Use this mode when panel switches are expensive (network, heavy compute) so
                        keyboard users aren't firing N requests as they navigate.
                    </p>
                </div>
                <div
                    class="tab-panel"
                    role="tabpanel"
                    id="man-c"
                    aria-labelledby="man-tab-c"
                    hidden
                >
                    <p>
                        Mouse click still activates immediately in either mode -- only keyboard
                        arrow-key behavior differs.
                    </p>
                </div>
            </brio-tabs>

Source comments

Tabs -- ARIA-tabbed content with a tablist + tabpanels structure.

Activation mode (JS-only behavior, on the custom element):
  <brio-tabs manual>  Arrow keys move focus only; Enter / Space
                      activates. Use when activation has side effects
                      (network requests, heavy compute) that shouldn't
                      fire on every arrow press.
  default              Arrow keys move focus AND activate -- the
                      conventional ARIA tab pattern.

Item state (per P5 -- ARIA over .is-* for interactive state):
  :hover, :focus-visible
  [aria-selected="true"]                  Active tab
  :disabled, [aria-disabled="true"]       Disabled tab (skipped by
                                           keyboard nav, no click)
  tabindex="-1"                           Inactive tab (roving tabindex
                                           managed by brio-tabs)

Markup:
  <brio-tabs class="tabs">
      <div role="tablist">
          <button class="tab" type="button" role="tab"
                  aria-selected="true" aria-controls="p1" id="t1">One</button>
          <button class="tab" type="button" role="tab"
                  aria-controls="p2" id="t2" tabindex="-1">Two</button>
      </div>
      <div class="tab-panel" role="tabpanel" id="p1" aria-labelledby="t1">...</div>
      <div class="tab-panel" role="tabpanel" id="p2" aria-labelledby="t2" hidden>...</div>
  </brio-tabs>

Archetype: ARIA-first subtype of Slotted container (P3). State lives in
ARIA attributes; CSS selectors bind to those attributes; a custom
element handles the keyboard / focus orchestration. The .tab and
.tab-panel classes are styling-equivalent to their ARIA roles, paired
the same way .dropdown-item is paired with [role="menuitem"] -- the
role is the API, the class is a parallel hook.

Reference

Source: src/css/tabs.css

Applies to

.tabs

Classes

ClassKindDescription
.is-verticalvariantSide-tab orientation with column tablist
.is-stretchedvariantTabs grow equally to fill the tablist width
.is-pillsvariantPill-shaped triggers with filled active state

Slots

SlotDescription
.tabs [role="tablist"]Tablist -- styled via the role since it's a structural container, * not a separately-named slot. Inset shadow is a deliberate choice: * the active tab's bottom-border indicator overlays cleanly without * a negative margin, which would get clipped by overflow-x scrolling.
.tabs :is([role="tab"], .tab)Tab -- both .tab class and [role="tab"] are styling hooks. Same * dual-pairing pattern as .dropdown-item + [role="menuitem"].

Tokens

TokenDefaultDescription
--tabs-font-sizevar(--ui-font-size-sm)Font size for tab triggers
--tabs-padding-inlinevar(--ui-pad-inline-compact)Inline-axis padding for tab triggers
--tabs-padding-blockvar(--ui-pad-block-tight)Block-axis padding for tab triggers
--tabs-gapvar(--ui-gutter-tight)Gap between tab triggers
--tabs-colorvar(--color-text-muted)Text color for inactive tabs
--tabs-color-activevar(--color-primary)Text color for the active tab
--tabs-border-colorvar(--color-border)Border color for the tablist underline
--tabs-border-color-activevar(--color-primary)Border color for the active tab indicator
--tabs-panel-padding-blockvar(--space-md)Block-axis padding for tab panels
--tabs-vertical-min-width10remMinimum inline size of the vertical tablist