Skip to content

lucasmotta/focusgroup

Repository files navigation

focusgroup-polyfill focusgroup-polyfill

focusgroup-polyfill

A polyfill for the HTML focusgroup attribute — declarative arrow-key navigation for composite widgets, no JavaScript required.

The focusgroup attribute is an Open UI proposal that lets you add keyboard navigation to toolbars, tablists, menus, grids, and other composite widgets with a single HTML attribute. This polyfill brings that behavior to browsers today.

Install

npm install focusgroup-polyfill

Usage

Script tag (auto-initializes)

<script src="https://unpkg.com/focusgroup-polyfill/dist/index.global.js"></script>

<div focusgroup="toolbar wrap" aria-label="Formatting">
  <button>Bold</button>
  <button>Italic</button>
  <button>Underline</button>
</div>

ES module

import "focusgroup-polyfill";

The polyfill auto-initializes on import. It's a no-op if the browser supports focusgroup natively.

Next.js (App Router)

For Next.js App Router, add an instrumentation-client.ts file at the project root (or under src/ if that is where your app lives). Next.js 15.3+ is required for this convention.

import "focusgroup-polyfill";

Manual control

import { init, destroy, isSupported } from "focusgroup-polyfill";

if (!isSupported()) {
  init();

  // Later, to tear down:
  destroy();
}

Behaviors

The first token in the focusgroup attribute specifies the behavior:

Behavior Navigation Default modifiers Roles applied
toolbar Left/Right inline toolbar
tablist Left/Right, wraps inline wrap tablisttab
radiogroup All arrows, wraps wrap radiogroupradio
listbox Up/Down block listboxoption
menu Up/Down, wraps block wrap menumenuitem
menubar Left/Right, wraps inline wrap menubarmenuitem
tree Up/Down block treetreeitem
grid 2D arrows

Modifiers

Add modifiers after the behavior token, separated by spaces:

<div focusgroup="toolbar wrap nomemory"></div>
Modifier Effect
inline Restrict to inline-axis arrows (Left/Right in LTR)
block Restrict to block-axis arrows (Up/Down in horizontal)
wrap Wrap from last item to first (and vice versa)
nowrap Disable wrapping (overrides behavior defaults)
nomemory Don't remember last-focused item on re-entry

Grid modifiers

<div focusgroup="grid row-flow col-wrap"></div>
Modifier Effect
wrap Wrap both rows and columns
flow Flow both axes (end of row → start of next row)
row-wrap / col-wrap Per-axis wrapping
row-flow / col-flow Per-axis flow
row-none / col-none Per-axis hard stops

Examples

Toolbar

<div focusgroup="toolbar wrap" aria-label="Actions">
  <button>Cut</button>
  <button>Copy</button>
  <button>Paste</button>
</div>

Arrow keys navigate between buttons. Wrap sends focus from last → first.

Tablist

<div focusgroup="tablist nomemory" aria-label="Settings">
  <button aria-selected="true" aria-controls="general" focusgroupstart>General</button>
  <button aria-selected="false" aria-controls="advanced">Advanced</button>
</div>
<div id="general" role="tabpanel">...</div>
<div id="advanced" role="tabpanel" hidden>...</div>

focusgroupstart determines which tab receives focus on entry. nomemory ensures it always returns to that tab.

Grid (CSS Grid)

<div
  focusgroup="grid wrap"
  role="grid"
  style="display: grid; grid-template-columns: repeat(4, 1fr);"
>
  <button>A1</button>
  <button>A2</button>
  <button>A3</button>
  <button>A4</button>
  <button>B1</button>
  <button>B2</button>
  <button>B3</button>
  <button>B4</button>
</div>

The polyfill reads grid-template-columns to determine the column count and maps arrow keys to 2D navigation. The container must be display: grid — a console warning is emitted otherwise.

Nested focusgroups

<div focusgroup="menubar" aria-label="App">
  <button>File</button>
  <button>Edit</button>
  <div focusgroup="toolbar" aria-label="Quick Actions">
    <button>Save</button>
    <button>Undo</button>
  </div>
  <button>Help</button>
</div>

Each focusgroup navigates independently. The inner toolbar's arrow keys don't affect the menubar.

Opt-out

<div focusgroup="toolbar">
  <button>A</button>
  <div focusgroup="none">
    <button>Excluded from arrow navigation</button>
  </div>
  <button>B</button>
</div>

Features

  • Roving tabindex — exactly one item per focusgroup is in the Tab order
  • Last-focused memory — re-entering via Tab restores the last focused item
  • focusgroupstart — specify which item receives focus on initial entry
  • Writing mode aware — arrow keys follow direction (LTR/RTL) and writing-mode
  • Home/End — jump to first/last item
  • Key conflict detection — arrow keys pass through to inputs, textareas, contenteditable; Tab/Shift+Tab escapes to the next focusgroup item
  • Role inference — container and <button> children get ARIA roles from the behavior token
  • Shadow DOM — works across shadow boundaries
  • Dynamic DOM — MutationObserver picks up added/removed items
  • Feature detection — no-op when native focusgroup is available

Playground

npm install
npm run dev

Opens at http://localhost:8787.

Development

npm install
npm run build         # Build ESM, CJS, and IIFE bundles
npm test              # Unit tests (Vitest)
npm run test:e2e      # E2E tests (Playwright)
npm run typecheck     # TypeScript check
npm run lint          # oxlint
npm run format        # oxfmt (write)
npm run format:check  # oxfmt (check only)
npm run ci            # Run the full CI pipeline locally

Releasing

Releases are managed by Changesets.

For contributors — add a changeset to every PR that touches user-facing code:

npx changeset

The CLI asks for the semver bump (patch / minor / major) and a short summary. It writes a .changeset/<random>.md file — commit it with your PR.

For internal-only changes (CI tweaks, refactors with no user impact), use npx changeset --empty so the check passes without producing a version bump.

Release flow (maintainer) — merging to main with pending changesets causes the release bot to open a "Version Packages" PR that bumps the version and updates CHANGELOG.md. Merging that PR publishes to npm and creates a GitHub release.

Browser support

The polyfill targets ES2020 and works in all modern browsers. Playwright tests run against Chromium.

License

MIT

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors