v1.0.0

<bmx-tabs>

One panel showing at a time, with a strip of tabs to choose it. Write the panels and nothing else:

7 properties · 1 events · 2 methods · 7 parts

Example

The panel's content is written where the panel is, and the tab is drawn from this element's label. There is no second element to keep in step and no pair of ids to wire up.

A badge is a short count or status, and it belongs to the panel rather than to the tab.

Give a panel a value when the label is likely to change — a translated label would otherwise change what the tabs report.

Not reachable while it is disabled.

Arrow keys move between tabs and choose as they go. Tab moves into the panel.

Vertical, in the pill appearance, and choosing only on Enter:

General settings. Notification settings. Security settings.
Show markup
<div class="row">
  <bmx-tabs id="ex-tabs" label="Project" style="max-inline-size: 40rem">
    <bmx-tab-panel label="Overview">
      <p style="margin: 0">
        The panel's content is written where the panel is, and the tab is drawn from this element's
        <code>label</code>. There is no second element to keep in step and no pair of ids to wire up.
      </p>
    </bmx-tab-panel>

    <bmx-tab-panel label="Members" badge="12">
      <p style="margin: 0 0 0.75rem">A badge is a short count or status, and it belongs to the panel rather than to the tab.</p>
      <bmx-input label="Find a member" placeholder="Name or email"></bmx-input>
    </bmx-tab-panel>

    <bmx-tab-panel label="Billing" value="billing">
      <p style="margin: 0">
        Give a panel a <code>value</code> when the label is likely to change — a translated label would otherwise change
        what the tabs report.
      </p>
    </bmx-tab-panel>

    <bmx-tab-panel label="Audit log" disabled>
      <p style="margin: 0">Not reachable while it is disabled.</p>
    </bmx-tab-panel>
  </bmx-tabs>
</div>

<div class="row">
  <span class="note" id="ex-tabs-out">Arrow keys move between tabs and choose as they go. Tab moves into the panel.</span>
</div>

<div class="row">
  <p style="margin: 0">Vertical, in the pill appearance, and choosing only on Enter:</p>
</div>

<div class="row">
  <bmx-tabs orientation="vertical" appearance="pill" activation="manual" label="Settings" style="max-inline-size: 40rem">
    <bmx-tab-panel label="General">General settings.</bmx-tab-panel>
    <bmx-tab-panel label="Notifications">Notification settings.</bmx-tab-panel>
    <bmx-tab-panel label="Security">Security settings.</bmx-tab-panel>
  </bmx-tabs>
</div>

<script type="module">
  await customElements.whenDefined('bmx-tabs');

  const tabs = document.getElementById('ex-tabs');
  const out = document.getElementById('ex-tabs-out');

  // The event carries the value, the position and the panel element itself -
  // the last of those for anything that needs to reach into the panel, such as
  // fetching its content the first time it is shown.
  tabs.addEventListener('bmxChange', event => {
    out.textContent = `bmxChange: value="${event.detail.value}", index ${event.detail.index}`;
  });
</script>
<bmx-tabs value="members">
  <bmx-tab-panel label="Overview">…</bmx-tab-panel>
  <bmx-tab-panel label="Members" badge="12">…</bmx-tab-panel>
</bmx-tabs>

WHERE THE IDS LIVE, AND WHY IT DECIDED THE SHAPE OF THE API

The tabs pattern is held together by two IDREFs - a tab points at its panel with aria-controls, the panel points back with aria-labelledby - and an IDREF does not cross a shadow boundary. That single fact ruled out the three-element design most libraries use, where the author writes the tabs and the panels as siblings: the strip is drawn here, inside this component's shadow root, so a tab in the author's tree could never see the panel and the reference would resolve to nothing. Not throw - resolve to nothing, which looks perfect on screen and fails an audit.

So both ends stay in this root: the tabs are drawn here, and so is the region that holds the panels. The author's bmx-tab-panel elements are slotted into that region, which puts them inside it in the flattened tree - the same reasoning that put role="radiogroup" on a <div> in bmx-radio-group rather than on its host.

One region rather than one per panel, and that is deliberate too: only one panel is ever showing, so one role="tabpanel" labelled by whichever tab is selected describes exactly what is on the screen. Every tab's aria-controls names that region.

ACTIVATION

auto (the default) selects as the arrows move, which is what WAI-ARIA recommends and what a native tab strip does. manual moves focus and waits for Enter or Space - the right choice when a panel fetches its content, where arrowing across four tabs to reach the fifth would fire four requests.

The tab stop follows the selection rather than the focus, so tabbing away and back returns to the panel that is showing rather than to wherever the arrows were last left.

Properties

PropertyAttributeTypeDefaultDescription
activation activation BmxPanelActivation 'auto' Whether the arrows choose as they move, or only move.
appearance appearance BmxTabsAppearance 'underline' How the strip is drawn.
label label string 'Tabs' The tab list's accessible name.
orientation orientation BmxOrientation 'horizontal' Which way the strip runs. Vertical moves on Up and Down instead.
size size BmxSize 'md' Size step, applied to the tabs.
stretch stretch boolean false Whether the tabs share the strip's width equally.
value value string The panel showing, by value. Mutable, so pressing a tab updates it. A consumer driving it may treat it as controlled and watch bmxChange. A value naming a panel that does not exist - or one that has been removed, or disabled - falls back to the first panel that can be shown rather than leaving the component blank.

Events

EventDetailDescription
bmxChange BmxTabsChangeDetail Fired when a different panel is chosen.

Methods

MethodSignatureDescription
setFocus setFocus(options?: FocusOptions) => Promise<void> Focus the selected tab.
showPanel showPanel(value: string) => Promise<void> Show a panel by value. Ignored if nothing has that value, or it is disabled.

Slots

SlotDescription
(default) bmx-tab-panel elements.

CSS shadow parts

PartDescription
panels The region the panels are shown in.
tab One tab. The selected one also matches tab-selected.
tab-badge The badge within a tab.
tab-icon The icon within a tab.
tab-label The label within a tab.
tab-selected The selected tab.
tablist The strip of tabs.

CSS custom properties

PropertyDescription
--bmx-tab-color A tab's text colour when it is not selected.
--bmx-tab-indicator-size Thickness of the line marking the selected tab.
--bmx-tab-padding-block Space inside a tab, down.
--bmx-tab-padding-inline Space inside a tab, across.
--bmx-tab-radius Corner radius of a tab, in the pill and enclosed appearances.
--bmx-tab-selected-color A tab's text colour when it is.
--bmx-tabs-gap Space between one tab and the next.
--bmx-tabs-panel-padding Space between the strip and the panel below it.