Horizontal Tablist with Wrapping

A tab control using focusgroup for arrow-key navigation. The tablist behavior token implies inline wrap by default, so wrapping is active without an explicit wrap token. With nomemory and a little JavaScript to move focusgroupstart, re-entry always lands on the selected tab instead of the last-focused one.

focusgroup="tablist nomemory"
Try it: Use and to navigate between tabs. Press past the last tab to wrap to the first. Tab moves to the panel content. Focusing a tab selects that panel (selection-follows-focus). Tab away from the tablist, then Tab back in; focus returns to the selected tab, not the last one you arrowed to.

Overview

The focusgroup attribute gives composite widgets declarative arrow-key navigation without JavaScript.

View source
<div focusgroup="tablist nomemory"
     aria-label="Sections" class="tablist">
    <button type="button" class="tab selected"
         aria-selected="true" aria-controls="panel-overview"
         id="tab-overview" focusgroupstart>Overview</button>
    <button type="button" class="tab"
         aria-selected="false" aria-controls="panel-features"
         id="tab-features">Features</button>
    <button type="button" class="tab"
         aria-selected="false" aria-controls="panel-pricing"
         id="tab-pricing">Pricing</button>
    <button type="button" class="tab"
         aria-selected="false" aria-controls="panel-faq"
         id="tab-faq">FAQ</button>
</div>
<div role="tabpanel" id="panel-overview" aria-labelledby="tab-overview"
     tabindex="0" class="tabpanel">...</div>
<div role="tabpanel" id="panel-features" aria-labelledby="tab-features"
     tabindex="0" class="tabpanel" hidden>...</div>
<div role="tabpanel" id="panel-pricing" aria-labelledby="tab-pricing"
     tabindex="0" class="tabpanel" hidden>...</div>
<div role="tabpanel" id="panel-faq" aria-labelledby="tab-faq"
     tabindex="0" class="tabpanel" hidden>...</div>

Vertical Tablist

A vertical tablist using block axis for Up/Down arrow navigation.

focusgroup="tablist block wrap nomemory"
Try it: Use and to navigate tabs. Press past the last tab to wrap to the first. Left/Right arrows do nothing. Selecting a tab shows its panel.

General Settings

Configure general application preferences such as language, theme, and notifications.

View source
<div focusgroup="tablist block wrap nomemory"
     aria-orientation="vertical" aria-label="Settings" class="tablist-vertical">
    <button type="button" class="tab selected"
            aria-selected="true" aria-controls="vpanel-general"
            id="vtab-general" focusgroupstart>General</button>
    <button type="button" class="tab"
            aria-selected="false" aria-controls="vpanel-privacy"
            id="vtab-privacy">Privacy</button>
    <button type="button" class="tab"
            aria-selected="false" aria-controls="vpanel-advanced"
            id="vtab-advanced">Advanced</button>
</div>
<div role="tabpanel" id="vpanel-general" aria-labelledby="vtab-general"
     tabindex="0" class="tabpanel">...</div>
<div role="tabpanel" id="vpanel-privacy" aria-labelledby="vtab-privacy"
     tabindex="0" class="tabpanel" hidden>...</div>
<div role="tabpanel" id="vpanel-advanced" aria-labelledby="vtab-advanced"
     tabindex="0" class="tabpanel" hidden>...</div>

Long Content: A Possible Mitigation

Because tabs use selection-follows-focus, arrowing between tabs instantly swaps the panel. If a panel has more content than fits on screen, the user may never scroll through it; the next arrow press switches to a different panel entirely.

One possible mitigation: Constrain the panel height and make it a focusable scrollable region. After the panel appears, the user presses Tab to enter the panel, then uses arrow keys to scroll its content. Pressing Tab again exits the panel. Other approaches, such as not using selection-follows-focus, adding a "read more" link, or avoiding tabs entirely for long content, may be more appropriate depending on the use case.

focusgroup="tablist nomemory"
Try it: Use to switch tabs. Then press Tab to enter the panel. Use to scroll the long content. Press Tab again to move past the panel.

The Problem

When tabs use selection-follows-focus, each arrow press immediately switches the visible panel. This is efficient for short panels; the user can quickly scan all tabs without pressing Enter or Space.

But if a panel is tall, containing a lengthy article, a long form, or detailed documentation, the user may never scroll through it. Pressing swaps the panel to the next tab's content, and the previous panel's unread content is gone.

Unlike the accordion pattern where arrow keys might skip over expanded content, here the problem is different: the content is replaced rather than skipped. The user might not even realise there was more to read.

Making the panel a constrained, scrollable region with a visible scrollbar hints that more content exists, and lets the user deliberately enter the panel to read it.

View source
<div focusgroup="tablist nomemory"
     aria-label="Articles" class="tablist">
    <button type="button" class="tab selected"
            aria-selected="true" aria-controls="long-tpanel1"
            id="long-tab1" focusgroupstart>The Problem</button>
    <!-- more tabs ... -->
</div>
<div role="tabpanel" id="long-tpanel1" aria-labelledby="long-tab1"
     tabindex="0" class="tabpanel tabpanel-scrollable">
    <p>Long panel content...</p>
</div>

<style>
.tabpanel-scrollable {
    max-height: 12rem;
    overflow: auto;
    scroll-behavior: smooth;
}
</style>

Key Point: Navigation vs. Selection

focusgroup handles keyboard navigation only. It does not manage selection state (aria-selected), panel visibility, or any other application logic. A small amount of JavaScript is still needed for:

But you no longer need any JavaScript for focus management, tabindex roving, or keyboard event handlers for arrow keys. The browser handles the roving tab stop natively.