Library / SDK
discord/focus-rings avatar
discord/focus-rings

react-focus-rings, the focus indicator system Discord built for itself

A centralized system for displaying and stylizing focus indicators anywhere on a webpage.

1,132 stars27 forksTypeScriptMIT

At a glance

What is it?
This repository holds a centralised system for rendering and styling focus indicators anywhere on a page, published as the react-focus-rings package. It exists because Discord could not find a native alternative that produced clean, consistent rings across their keyboard navigation work, and the design that resulted has two components with one hard requirement.
Who is it for?
Adopt react-focus-rings if you are building a React application that needs one consistent, customisable focus indicator across a large surface, and you want the ring drawn by the library rather than by the browser default. Do not adopt it for a non-React codebase, and do not adopt it if you want a dependency free solution, since the package is React specific and the stylesheet must be imported separately.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 18 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 22, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The problem Discord could not solve with what existed

The motivation section is short and unusually honest about where the project came from. While working on a complete keyboard navigation experience for Discord, the team ran into a number of problems with rendering clean and consistent focus rings, and were unable to find any suitable native alternatives. After a lot of trial and error, they landed on this system.

That origin explains the design. The browser's native focus ring is a single, hard to restyle indicator, and an application with a custom design system, custom components and custom scrolling containers quickly finds that the default is either invisible or ugly depending on the surface behind it. Discord needed one ring that looked the same everywhere, including in places the browser default handles badly.

The result was extracted as a package, which is the right call for something built inside a large application. A rule that says no component may ship without a focus ring is hard to enforce by convention and easy to enforce by a wrapper, and a wrapper that renders a ring rather than a class name is harder to get wrong than a stylesheet that has to be applied in the right order.

The audience for the package is therefore React developers with the same problem, which is a different group from the audience for a general accessibility library. This is one specific solution to one specific rendering problem, and the documentation is written for people who already know what a focus ring is and why it is being drawn.

The project also points at a blog post describing how Discord implemented app-wide keyboard navigation, which is where the wider context lives.

Two components: the scope provides the frame, the ring draws itself

The library is composed of two components, FocusRingScope and FocusRing, and the split between them is the whole idea.

FocusRingScope is responsible for providing a frame of reference for calculating the position of any FocusRing instances it contains. The containerRef prop takes a React.Ref that references the DOM element used for those position calculations, and you are told to include a FocusRingScope instance at the top level of your application. If a component creates a new scroll container, or is absolutely positioned within the viewport, you should add a new FocusRingScope.

That last sentence is the operational rule. One scope at the root is the baseline, and additional scopes appear where the coordinate space changes.

FocusRing is the main show. You wrap any focusable element with a FocusRing and it adds the required focus and blur listeners and renders the focus ring when the element receives focus.

tsx
function Button(props: ButtonProps) {
  return (
    <FocusRing>
      <button {...props} />
    </FocusRing>
  );
}

The listeners are the mechanism. Rather than asking you to add a class on focus, the component attaches the handlers itself and manages the rendering, which is why the ring can be a drawn element positioned relative to the target instead of a CSS outline.

The recommendation from the project is to integrate at the design primitive level, in custom components like Button or Link, so the behaviour applies across the application with minimal effort. That is the difference between a library that is used correctly and one that is applied one call site at a time, and it is why a small package can change the accessibility of a whole product.

The containerRef element must be position: relative, or nothing lines up

There is one hard requirement in the documentation, and it is stated with the troubleshooting advice attached.

The element provided to containerRef must be styled with position: relative, or else the alignment calculations will be incorrect. If you find that a FocusRing is not being rendered at all or its positioning is wrong, the instruction is to verify that you have a FocusRingScope in the correct places and that the containerRef element has the position: relative style.

That is the most likely first failure for a new user, and it is a failure with two possible symptoms that point at the same cause. The scope needs a positioned ancestor so it can establish the origin it measures rings against, and without that origin the calculated coordinates have nothing meaningful to be relative to.

The two symptoms are worth distinguishing in practice, because they lead to different checks. A ring that never appears usually means the scope is missing or is not an ancestor of the element. A ring that appears in the wrong place usually means the scope is present but the container element is not positioned, so the offsets are being measured from the wrong box.

The scope example shows the shape of the fix.

tsx
function ScopeExample() {
  const containerRef = React.useRef<HTMLDivElement>(null);
  return (
    <div ref={containerRef} id="main">
      <FocusRingScope containerRef={containerRef}>
       {/* ... */}
      </FocusRingScope>
    </div>
  )
}

A reference created with React.useRef, attached to a div, and passed to the scope as containerRef. The div is where the position relative style goes.

The props that change what the ring does and where it points

FocusRing has a handful of props, and the documentation recommends using the exported type rather than hand writing it.

tsx
import {FocusRingProps} from 'react-focus-rings'

The within prop acts like the CSS :focus-within selector and renders the focus ring if any descendant is focused, which is how you get a ring around a card or a row rather than around the button inside it. The enabled prop controls whether the ring is rendered at all, and focused controls the focused state, both of which matter when your application has its own focus management or a modal that should suppress indicators behind it.

The offset prop adjusts the alignment of the focus ring relative to the focused element, and it accepts either a number or an Offset object. An Offset object lets you set the offset per side, which is what you need when the ring has to clear an irregular element.

Two props are a pair and must be used together. focusTarget lets you choose a different element to act as the focus target for the ring, and ringTarget lets you choose a different element to render the ring around. The documentation says both must be used with each other, and the reason is obvious once stated: separating the thing that receives focus from the thing the ring is drawn around is only meaningful if you say both, and the default of one element being both is not always what a design wants.

The last prop, focusWithinClassName, applies a CSS class to the focused element when a descendant is focused, like :focus-within, for the cases where a style change on the container is wanted in addition to the ring.

The stylesheet is a separate import and forgetting it breaks rendering

The focus ring also relies on some default CSS styles in order to render properly, and the instruction is to import the styles separately somewhere within the application.

tsx
import "react-focus-rings/src/styles.css";

That import path points into the package's source directory rather than a top level styles file, which is worth noticing because it means the package's internals are part of the public contract. A consumer is reaching into src/ to get the styles, so a change to that file's location would be a breaking change for every consumer, and the package's own version history shows the maintainers treat the package surface that way. One of the earlier releases, v1.0.8, is titled Fix default package export compatibility.

The failure mode of a missing stylesheet is the same one the troubleshooting note mentions: a FocusRing that is not being rendered at all. The component attaches the listeners and computes the position, but the styles are what make the drawn ring visible and correctly placed.

The complete minimal example in the documentation shows the whole shape of a working setup, and it is worth reading as a checklist of the pieces.

tsx
import * as React from "react";
import ReactDOM from "react-dom";

import { FocusRing, FocusRingScope } from "react-focus-rings";
import "react-focus-rings/src/styles.css";

Two imports from the package, one of them the stylesheet, and then a component that creates the ref, attaches it to a container div, and wraps the content in a FocusRingScope. A more complete example lives in the examples directory of the repository, and a hosted version of it is linked as well.

Published as react-focus-rings, developed in a pnpm workspace

The package is published under the name react-focus-rings and can be installed with any npm-compatible package manager. That is the name you depend on, and it is worth being careful about it, because the repository's own root package.json is a different thing: it is named react-focus-rings-base, marked private, and holds the workspace configuration with the format, lint and lint:fix scripts, the pnpm package manager pinned at 9.12.2, and the TypeScript and ESLint dev dependencies.

The repository is a pnpm workspace, with a pnpm-lock.yaml and a pnpm-workspace.yaml at the top level, a packages directory, and separate tsconfig files for the project and for the node tooling. The published package itself lives under packages/.

The release history shows a project that reached 1.1.0 in September 2022 and then stopped tagging. The releases are v1.1.0, titled Toggle ring visibility through JS, from 2022-09-19, v1.0.8, Fix default package export compatibility, from 2022-09-15, and v1.0.7 from 2022-09-14. The last push to the repository was on 2026-10-05 and the repository is not archived.

So the maintenance picture is a package whose published versions stopped more than three years ago while the repository itself is still being touched. The licence is MIT, and the description of the package says nothing more than that it is a centralised system for displaying and stylizing focus indicators anywhere on a webpage.

The practical question for an adopter is whether the pinned version you resolve still does what you need, since the props and the stylesheet path are the contract and the last tagged release is where that contract was last versioned.

Editorial conclusion

Adopt react-focus-rings if you are building a React application that needs one consistent, customisable focus indicator across a large surface, and you want the ring drawn by the library rather than by the browser default. Do not adopt it for a non-React codebase, and do not adopt it if you want a dependency free solution, since the package is React specific and the stylesheet must be imported separately. Before you build on it, read the position relative requirement for the containerRef element, because a missing FocusRingScope or an unstyled container is the documented cause of a ring that fails to render or is misaligned, and check that the published react-focus-rings package, not the private base package in this repository, is what you depend on.

Frequently asked questions

What are the two components in react-focus-rings?

FocusRingScope provides a frame of reference for calculating the position of the rings it contains, using a containerRef prop, and FocusRing wraps a focusable element, adds the focus and blur listeners and renders the ring when the element is focused.

Why is my focus ring not rendering or is positioned wrongly?

Check that you have a FocusRingScope in the correct places and that the element passed to containerRef is styled with position: relative, since the alignment calculations are incorrect otherwise.

How do I install react-focus-rings and import its styles?

The package is published as react-focus-rings and can be installed with any npm-compatible package manager. The styles must be imported separately in the app with `import "react-focus-rings/src/styles.css";` for the ring to render properly.

Which props let me draw the ring around something other than the focused element?

focusTarget chooses the element that acts as the focus target and ringTarget chooses the element the ring is rendered around, and the documentation says both must be used together. The within prop works like :focus-within and renders the ring when any descendant is focused.

Why did Discord build this instead of using an existing focus ring library?

While building a complete keyboard navigation experience, the team found problems with rendering clean and consistent focus rings and could not find suitable native alternatives, so after trial and error they landed on this system and published it as react-focus-rings.

Official sources

  1. discord/focus-rings on GitHub
  2. Issues
  3. License: MIT
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/discord-focus-rings.svg)](https://hysenlabs.com/projects/discord-focus-rings)

Was this useful?