Keyboard & focus management
What tabindex 0, -1, and positive values do
Focusable and tabbable are two different properties. Tabbable means the element is in the sequence you reach by pressing Tab; focusable means script or a click can put focus on it. Native interactive elements (<a href>, <button>, <input>, <select>, <textarea>) are both by default, ordered by their position in the DOM.
tabindex changes this:
tabindex="0"puts a non-interactive element into the natural tab order at its DOM position. It becomes tabbable.tabindex="-1"(any negative value) removes an element from the Tab sequence but keeps it focusable from script viaelement.focus()or a click. Focusable, not tabbable.- A positive value (
tabindex="3") jumps the element ahead of the natural order. Use only0and-1; a positive value is an anti-pattern.
Wrong "tabindex="-1" hides the element from keyboard users." It only takes the element out of the Tab sequence. You still focus it programmatically, which is exactly how modals and route-change targets receive focus. Right reserve -1 for things you will focus with script, and never add tabindex="0" to a heading or image so users will notice it. Static content already gets read; an extra tab stop with nothing to do is a dead stop.
Why a clickable div fails keyboard users
A <div onclick> is neither focusable nor operable by keyboard. To match a button you would need tabindex="0", a role, and keydown handlers for Enter and Space. A native <button> gives all of that for free, so reach for it first. The wider contract: Tab and Shift+Tab move between controls, arrow keys move within a composite widget (a toolbar, a set of tabs), and Enter or Space activates. Any handler bound to a mouse event needs a keyboard equivalent, or the control works for pointer users only.
Show the focus ring only when it helps
:focus matches on any focus, including a mouse click. :focus-visible matches only when the browser decides a ring would help, which in practice means keyboard and scripted focus, not a mouse click on a button. That difference is why removing the ring is such a common mistake.
Wrong ":focus { outline: none } cleans up the ugly ring." It removes the indicator for keyboard users too, and a sighted keyboard user can no longer see where they are. Right scope it. :focus:not(:focus-visible) { outline: none } drops the ring for mouse clicks while :focus-visible { outline: 2px solid } keeps it for keyboard navigation. Never delete the outline without a visible replacement (a border, box-shadow, or background). A page with no visible focus indicator fails the keyboard-accessibility bar.
Roving tabindex vs aria-activedescendant
A composite widget (toolbar, tablist, radio group, menu, grid, listbox) must be a single tab stop, not one per item; tabbing through a fifty-item listbox one press at a time is the failure this prevents. Two mechanisms exist.
Roving tabindex moves real DOM focus. Exactly one item carries tabindex="0" and every other carries -1. On an arrow key you set the active item to -1, set the destination to 0, then call .focus() on it, in that order. Remember the last active item so Tab re-enters where the user left. Because real focus moves, :focus-visible and the browser's scroll-into-view work for free.
aria-activedescendant keeps DOM focus on the container and points at the active child's id. It fits a combobox where focus must stay in the text input, but you draw the focus indicator yourself and it depends on assistive tech honoring the attribute.
The three obligations of a modal focus trap
A dialog owes three things: move focus in on open (the first control, or a tabindex="-1" heading for a long dialog), keep Tab and Shift+Tab wrapping inside while the background is inert, and restore focus to the triggering element on close. Save that trigger before opening; forget the restore and focus drops to <body>, landing the user at the top of the page.
Native <dialog>.showModal() handles most of it: top layer, a ::backdrop, automatic inert on the rest of the page, Escape to close, and focus restored to the invoking element on close. The only gap is when that invoker was removed from the DOM or is no longer focusable, where you name a fallback target yourself. For a hand-rolled modal, the inert attribute on everything outside the dialog is what stops Tab from escaping.
Wrong "Any focus trap fails WCAG No Keyboard Trap (2.1.2)." That rule forbids the trap you cannot escape. An intentional modal trap is allowed as long as a standard key such as Escape leaves it. Right a modal must always be dismissible by keyboard; the violation is the broken embed that eats Tab with no way out.
Moving focus on a SPA route change
A full page load resets focus to the top and the screen reader announces the new page. A client-side router swaps the DOM with no load, so focus stays on the removed link or nowhere, and the user is stranded with no announcement. On each route change, move focus: give the new view's container or <h1> a tabindex="-1" and call .focus(), which also prompts the reader to announce the heading. Many routers, including bare React Router, do not do this for you. A skip link as the first focusable element (<a href="#main">Skip to main content</a>, visually hidden until focused) lets keyboard users jump past repeated navigation on every page.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.