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).
Overview
The focusgroup attribute provides
declarative arrow-key navigation for composite widgets
without JavaScript.
Features
Arrow-key navigation, wrapping, axis locking, focus memory, grid navigation, and more — all with a single HTML attribute.
Pricing
It's free! focusgroup is a native HTML
attribute — no libraries or frameworks needed.
FAQ
Q: Does focusgroup handle tab panel
switching?
A: No. focusgroup
only handles keyboard navigation. Panel switching and
aria-selected state must be managed with
JavaScript.
inline wrapenables Left/Right navigation with wrap-aroundno-memoryensures re-entry always goes to the selected tab, not the last-focused onefocusgroupstarton the selected tab marks the entry point- JavaScript handles panel visibility and
aria-selectedstate — focusgroup handles navigation
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.
General Settings
Configure general application preferences such as language, theme, and notifications.
Privacy Settings
Manage your privacy preferences, data collection, and cookie settings.
Advanced Settings
Advanced configuration options for power users and developers.
blockrestricts navigation to Up/Down arrows- Pair with
aria-orientation="vertical"for screen readers - Same
wrapandno-memorybehavior as the horizontal version
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.
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.
The Mitigation
Step 1: Constrain the panel height
Set max-height and overflow:
auto on the
tabpanel. This makes long content scrollable and shows a
scrollbar
as a visual cue that more content exists.
Step 2: Make the panel focusable
Add tabindex="0" to the tabpanel. When the
user presses
Tab from the tablist, focus enters the panel.
Arrow keys
then scroll the panel content instead of switching tabs.
Step 3: Tab to exit
Because the panel is outside the focusgroup, pressing Tab exits it normally. The user can continue to the next focusable element on the page, or Shift+Tab back to the tabs.
This approach preserves the fast tab-switching behavior while giving the user an explicit way to consume long content.
When To Use This Pattern
Not every tablist needs scrollable panels. Consider this pattern when:
- Panel content is likely taller than the viewport
- Panels contain long-form text, articles, or documentation
- Panels contain forms or many interactive controls the user must complete
- You want the visible scrollbar to signal "there's more to read"
For short panels (a heading and a paragraph), the standard tablist pattern from Demo 1 is ideal. The user sees all content at a glance and can arrow freely between tabs.
The key question is: will the user miss important content if they arrow to the next tab? If yes, constrain the panel height so the scrollbar provides a visual cue.
- Panels are scrollable regions (
tabindex="0"+max-height+overflow: auto) - Tab from the tablist enters the panel — arrow keys then scroll content
- The visible scrollbar hints that more content exists below the fold
- This preserves fast tab switching while preventing content from being missed
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:
- Toggling
aria-selectedon tabs - Showing/hiding tab panels
- Moving
focusgroupstartto the selected tab (sono-memoryre-entry lands on the active tab)
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.