The most important piece of effective keyboard navigation is being able to know where focus currently sits on the page and visualizing when that focus moves. This indicator is referred to as a Focus Ring.

The following sections provide background information on our requirements for an effective focus ring and how various approaches fail to satisfy them. The final suggested API is outlined in the Proposed API section at the end, and can be read independently.

Approach

Browsers implement default focus rings that apply to all elements, but the ability to style these is (currently) very limited. These rings, while they have recently improved greatly in Chrome and Edge, are also not very pleasant when integrated with the rest of Discord's design, and other browsers like Firefox are almost entirely invisible in most cases due to the thinness and low contrast of their styles.

As such, we want to implement a custom focus ring style. At a glance, this seems relatively simple, but when dealing with the variety of use cases Discord has for these rings, the list of requirements quickly grows, and the options for implementations shrink.

Ideally, we want to match the browser's focus ring behavior exactly. Within Discord, this means that a comprehensive focus ring implementation needs to:

Additionally, to be able to implement pleasant and overall better focus styles for various elements in the app, we have additional requirements to be able to:

Given these requirements, we can narrow down possible implementations.

per-element rendering with &:focus

This implementation is the simplest, and currently how focus rings are implemented in the experiment. All of the behavior is still managed by the browser. However, it fails to meet a majority of the design requirements to be effective as a universal method for applying focus rings.

The first failure is with clipping when overflow: hidden is applied on a container. Even with pseudo elements and absolute positioning, the ring is still going to be applied within the container, and so it is subject to clipping: