Use the platform. Reach for a native API before a library.
Move focus intentionally when the UI changes. Prevent focus traps. Use #stage as a skip-link
replacement. Use scroll-mt to keep targets visible below sticky headers.
When new content appears (an inline form, a confirm dialog, a status message), focus should follow. A user who can't see the screen has no other way to know something changed. Equally important: focus must never get stranded in a region that's been removed or hidden.
autoFocus on inline forms// When an edit form replaces a list item, focus the first field immediately
{isEditing ? (
<input autoFocus defaultValue={item.name} />
) : (
<span>{item.name}</span>
)}autoFocus on destructive confirmations// Move focus to the confirm button, not the cancel button
{showConfirm && (
<div>
<p>Delete "{item.name}"?</p>
<button onClick={cancel}>Cancel</button>
<button autoFocus onClick={confirm}>Delete</button>
</div>
)}#stage as skip-link replacement// Every nav link ends with #stage — keyboard users skip the nav by activating any link
<a href="/recipes#stage">Recipes</a>
// The main content area is the target
<main id="stage">…</main>
// Same-page: scroll smoothly, don't navigate
function handleNavClick(e, href) {
if (currentPath === href) {
e.preventDefault();
document.getElementById('stage')?.scrollIntoView({ behavior: 'smooth' });
}
}/* Prevent section headings from hiding under a sticky nav on hash navigation */
.section-heading { scroll-margin-top: 5rem; /* ~80px */ }<h2 className="scroll-mt-20" id="section-id">Section</h2>// Store the trigger ref before opening; restore on close
const triggerRef = useRef<HTMLButtonElement>(null);
function close() {
setOpen(false);
triggerRef.current?.focus();
}
<button ref={triggerRef} onClick={() => setOpen(true)}>Open</button>book-cover-nav.md — #stage anchor is the scroll target all nav links point toaria-patterns.md — aria-describedby IDs need to remain in the DOM when their button is focusedpackages/app/components/Nav.tsx, packages/app/components/pages/CookwarePage.tsx in pantry-host