focus management

Keyboard Navigation & Focus Management — Live Demos

5 interactive examples

tabindex Visual Explorer — see tab order in real time

Press Tab to move through the elements below. The active element highlights in blue and shows its tab order position. Greyed elements with dashed borders have tabindex="-1" — Tab skips them.

Tab through these elements ↓
Link 1 tabindex="-1" (skipped)
div — no tabindex (skipped)
div[tabindex=0] 5
--:--:--Press Tab inside the area above to navigate

The three tabindex values

✅ tabindex="0" In tab order at DOM position. Use for custom interactive elements. ✅ tabindex="-1" Not in tab order. .focus() can target it. Use for: modals, skip targets.
❌ tabindex="1" ❌ tabindex="2" ❌ tabindex="99" Positive values force an unnatural tab order. NEVER use positive values.

:focus vs :focus-visible vs :focus-within

Try clicking the buttons and Tab-navigating to them. See the difference in when each pseudo-class shows a ring.

❌ Using :focus (shows ring on mouse click too)

⚠ Mouse click shows focus ring — distracting for mouse users

✅ Using :focus-visible (smart ring)

✓ Mouse click: no ring. Tab navigation: ring shows. Best of both.

:focus-within — style the parent when a child is focused

The entire row highlights (background + label color) when its input is focused. This is pure CSS — no JavaScript. The :focus-within pseudo-class applies to the row when any descendant has focus.

.fw-row:focus-within { background: #f0f9ff; /* highlights row */ } .fw-row:focus-within label { color: #0ea5e9; /* changes label color */ font-weight: 500; }

Skip link — Tab to reveal, click to jump focus to main content

Tab into the demo area below. The skip link appears. Click it (or press Enter) — focus jumps directly to the main content, not the first nav link.

Skip to main content

Main content

Focus landed here directly, skipping all 4 nav links. The tabindex="-1" on this <main> element is the Chrome bug fix.

--:--:--Tab into the blue nav area above to see the skip link

CSS visibility trap — which methods block focus?

Click each card to Tab into the hidden element. Green = Tab skips it. Red = Tab still reaches it (keyboard trap).

display: none

✅ Tab skips

visibility: hidden

✅ Tab skips

opacity: 0

❌ Tab REACHES it

clip-path

❌ Tab REACHES it
--:--:--Click each card and Tab — see which methods trap keyboard focus

inert attribute — focus trap with zero JavaScript

Click "Open dialog". The page content becomes inert — completely unreachable by Tab, mouse, and screen reader. Focus is trapped inside the dialog automatically.

Page content:
--:--:--Click "Open dialog" then try Tab — focus stays inside the dialog

The complete code comparison

❌ Old: 50-line JS focus trap
const focusable = el.querySelectorAll( 'a,button,input,…' ).filter(…); const first = focusable[0]; const last = focusable.at(-1); el.addEventListener('keydown', e => { if(e.key!=='Tab') return; if(e.shiftKey && doc.activeElement===first){ e.preventDefault(); last.focus(); } else if(!e.shiftKey && doc.activeElement===last){ e.preventDefault(); first.focus(); } } );
✅ New: inert (1 line)
// Open modal: pageContent.inert = true; dialog.showModal(); // Close modal: pageContent.inert = false; dialog.close(); trigger?.focus(); // inert blocks: // ✓ Tab / Shift+Tab // ✓ Mouse clicks // ✓ Screen readers // ✓ Page search Ctrl+F

Roving tabindex — Arrow keys navigate within a widget

Tab into the toolbar. Use keys to move between buttons. Home / End jump to first/last. Only ONE button is in the tab order at a time.

Active: Bold · tabindex="0" on this button, "-1" on all others
--:--:--Tab into the toolbar, then use ← → Arrow keys and Home/End

Menubar — same pattern, different widget

Tab to reach the menubar. Arrow keys move between menu items. Each menu item gets tabindex="0" when active, "-1" when inactive.

--:--:--Tab to menubar, use ← → arrows to navigate
Read the tutorial