Many users never touch a mouse — make every interaction reachable with Tab, keep a visible focus ring, and manage focus in modals so keyboard users never get lost.
Why: the focus ring is the outline showing which element the keyboard is on. A common mistake is removing it for looks (outline: none) — which leaves keyboard users with no idea where they are. Note: if you dislike the default ring, replace it with a nicer one using :focus-visible; never just delete it.
/* ❌ Never do this with nothing to replace it */
button:focus { outline: none; }
/* ✅ Show a clear ring only for keyboard focus (not mouse clicks) */
button:focus-visible {
outline: 2px solid #2563eb;
outline-offset: 2px;
}Why: the order users Tab through elements is the order they appear in the HTML — so a logical source order is a logical focus order. Avoid positive tabindex values (tabindex="1"), which hijack the order and almost always cause bugs. Note: tabindex="0" puts a custom element in the natural order; tabindex="-1" makes it focusable only via script (useful for moving focus), never reachable by Tab.
<!-- tabindex="0": include a custom widget in the natural tab order -->
<div role="button" tabindex="0">Custom control</div>
<!-- tabindex="-1": focusable by script only — e.g. a heading you'll
move focus to after navigation, but that users won't tab onto -->
<h1 tabindex="-1">Page title</h1>
<!-- ❌ Avoid: positive values override natural order and break things -->
<input tabindex="3" />Why: when a dialog opens, focus should move into it, stay trapped inside while it is open, and return to the trigger when it closes — otherwise a keyboard user is left interacting with the hidden page behind it. Note: the native <dialog> element does most of this for you, including trapping focus and closing on Escape.
const dialog = document.querySelector('dialog')
const openBtn = document.querySelector('#open')
// showModal() moves focus inside, traps Tab, and enables Escape-to-close
openBtn.addEventListener('click', () => dialog.showModal())
// On close, send focus back to the button that opened it
dialog.addEventListener('close', () => openBtn.focus())Why: keyboard users hit the same long header nav on every page before reaching the content. A skip link — the first focusable thing on the page — lets them jump straight to <main>. It can stay visually hidden until focused. Note: this is a WCAG requirement (bypass blocks) and takes about five lines.
<body>
<a href="#main" class="skip-link">Skip to content</a>
<header>…long navigation…</header>
<main id="main" tabindex="-1">…</main>
<style>
/* Hidden until a keyboard user tabs to it, then it appears */
.skip-link { position: absolute; left: -9999px; }
.skip-link:focus { left: 1rem; top: 1rem; }
</style>
</body>