When working on creating a complete keyboard navigation experience for Discord, using styling with :focus
and outline
, the folks at Discord ran into issues where the outline would not match the shape of actual element being rendered. Thinks like border-radius
, overflow: hidden;
on the container, padding
got in the way. So they set out to find a solution.
After a lot of trial and error, we landed on a system which is built on two components:
FocusRing
for declaring where a ring should be placed, andFocusRingScope
for declaring a root position for rings to be rendered.
Here’s an example showing how the FocusRing
works:
function SearchBar() {
const containerRef = React.useRef<HTMLDivElement>(null);
const inputRef = React.useRef<HTMLInputElement>(null);
return (
<FocusRing focusTarget={inputRef} ringTarget={containerRef}>
<div className={styles.container} ref={containerRef}>
<input type="text" ref={inputRef} placeholder="Search" />
<div className={styles.icon}>
<ClearIcon />
</div>
</div>
</FocusRing>
);
}
The FocusRing
will capture focussing of the contained input
, but will render the ring around the entire div
. To have a FocusRing
behave like :focus-within
and respond to any descendant being focussed, you can set the within
prop.
The package can be installed using NPM:
npm i react-focus-rings
How Discord Implemented App-Wide Keyboard Navigation →
Browser Focus Ring Problems →react-focus-rings
(GitHub) →
Related: Not entirely coincidental the aforementioned React Spectrum by Adobe also comes with a FocusRing
component.