Skip to content

Don't Drop Focus When the Button Disappears

Naseebullah AhmadiSenior Software Engineer, London

Click "Show all" on a collapsed block and the button unmounts, taking keyboard focus with it: the next Tab starts from the top of the page. `useCollapsible` moves focus to whatever replaced the button.

4 min read
#frontend

When a focused element is removed from the page, the browser moves focus to <body>. A mouse user never notices. A keyboard or screen reader user is thrown back to the start of the page, a few hundred Tabs away from the code they just opened. The fix is to move focus yourself after the toggle, but only after this toggle: expanding from anywhere else (an "Expand all" control) shouldn't steal focus from where the user is.

@itsnas long-block.tsx
codelong-block.tsx
'use client'
 
function LongBlock({ children }: { children: ReactNode }) {
  const [expanded, { toggle: flip }] = useToggle()
  // @src/code/use-collapsible
  const { contentRef, showAllRef, toggle } = useCollapsible(expanded, flip)
 
  return (
    <div>
      <div
        className={expanded ? undefined : 'max-h-80 overflow-hidden'}
        ref={contentRef}
        tabIndex={-1}
      >
        {children}
      </div>
 
      {expanded ? (
        <button onClick={toggle} type="button">Collapse</button>
      ) : (
        <div className="fade-out">
          <button onClick={toggle} ref={showAllRef} type="button">
            Show all
          </button>
        </div>
      )}
    </div>
  )
}
main
Nas (@itsnas)

The content gets tabIndex={-1}: focusable from code, but not an extra Tab stop. Landing on it on expand puts the reader at the top of what they asked to see, and collapsing returns them to "Show all".