Horizontal Tablist with Wrapping

A tab control using focusgroup for arrow-key navigation. The wrap token enables wrap-around, and no-memory ensures focus always returns to the selected tab (not the last-focused one).

focusgroup="tablist inline wrap no-memory"
Try it: Use and to navigate between tabs. Press past the last tab to wrap to the first. Tab moves to the panel content. Focus on tab selects panel content.

Overview

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

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

Vertical Tablist

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

focusgroup="tablist block wrap no-memory"
Try it: Use and to navigate tabs. 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 no-memory"
     aria-orientation="vertical" aria-label="Settings">
    <button role="tab" class="tab selected" aria-selected="true"
            aria-controls="vpanel-general">General</button>
    <button role="tab" class="tab" aria-selected="false"
            aria-controls="vpanel-privacy">Privacy</button>
    <button role="tab" class="tab" aria-selected="false"
            aria-controls="vpanel-advanced">Advanced</button>
</div>
<div role="tabpanel" id="vpanel-general" tabindex="0">...</div>
<div role="tabpanel" id="vpanel-privacy" tabindex="0"
     hidden>...</div>
<div role="tabpanel" id="vpanel-advanced" tabindex="0"
     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 inline wrap no-memory"
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 inline wrap no-memory"
     aria-label="Articles">
    <button role="tab" class="tab selected" aria-selected="true"
            aria-controls="long-tpanel1">The Problem</button>
    <!-- more tabs ... -->
</div>
<div role="tabpanel" id="long-tpanel1" tabindex="0"
     class="tabpanel-scrollable">
    <p>Long panel content...</p>
</div>

<style>
.tabpanel-scrollable {
    max-height: 12rem;
    overflow: auto;
}
</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.