3 Layers of UI Interaction

It’s no surprise, dear reader, that most web interactions are lacking, and few interfaces have the level of polish they should. Part of that polish comes from being mindful of what I’ll refer to as “The 3 Layers of UI Interaction:”

  1. Render area: where does this element appear?
  2. Hit area: what is the shape/size/placement of the invisible interactive area?
  3. Focus area: when Tab-ing, what is the focus ring shape/placement?

We’ll skip over the first layer—render area—because it’s a given (if it exists, it’s “rendered”). While there are deeper nuances like affordances, that’s beyond the scope of this post. We only call out the first layer to point out that there are additional layers beyond this one.

It’s only once you realize the 2 other layers exist and are distinct from the first that you’ll create UI so fluid and smooth it might as well be invisible to the user. We can explore how all 3 layers diverge—and require separate decisions—in one simple example: a dialog “close” button.

Nothing you haven’t seen before. But before we get further, ask yourself: “How would I code this? What pixels count as part of the close button (and conversely, which pixels don’t)? What does the focus ring look like?” In fact, this may be something you’ve built dozens of times without being aware of some of the decisions you could make.

In practice, you’ve probably seen (or even built) naïve implementations such as…

“Baby’s first button”

The hit area (red) is too small! Making it the exact size and placement as the close icon gives the user little forgiveness in accuracy (which is an accessibility concern). If a user clicks in the corner above the close icon, nothing happens, which is almost certainly frustrating. The visible focus ring is there, but it’s pretty pathetic, and could be improved.

This implementation flattens all 3 layers together, resulting in a frustrating user experience. This is a common trap that probably every frontend dev has fallen into at the start of their career (often because the rendering part proved such a challenge there’s no time or mental energy left for the deeper interactions).

“The ableist”

The hit area is much better, with corner clicks now triggering the expected action. But the focus ring is an afterthought (assuming it hasn’t been disabled outright) because it’s not as closely-scoped to the shape it’s meant to be highlighting.

This second implementation correctly separates Layer 1 (rendering) from 2 (hit area), resulting in a pleasant interaction for mouse & touchscreen users. But it falls short of the ideal solution because it forgets about keyboard users that need Layer 3: focus area.


Screencap of the 1997 movie RocketMan starring Harland Williams
“We’ll enter in the same calculations using what we like to call ‘The Right Way’” (RocketMan)

While both of the previous implementations are functioning (barely), they’re not ideal, which is what we’re after. The best solution is a bit more complex:

The ideal solution involves the correct hit area around the close icon (shown as a dashed red line). We don’t want it to be too large so it doesn’t overlap text content a user may want to highlight. But we also want to prevent dead space, so we asymmetrically extend the hit area into the corner because we can assume any interaction there is an intent to close.

The focus ring being a tighter circle around the icon shows this interaction got equal time and consideration as the others, and shows keyboard users you care about them (note the circle shape is a design choice that doesn’t matter, but the attention to detail here does).

This is particularly annoying to code, because it will involve lots of tweaks to align everything properly:

.close-btn {
  /* settings (adjust all to taste) */
  --icon-size: 1rem;
  --hit-area-ratio: 2;
  --hit-area-offset: 0.25;
  --focus-ring-ratio: 0.125;

  /* rendering: align icon to corner */
  display: flex;
  align-items: flex-end;
  justify-content: flex-start;
  position: absolute;
  right: 0;
  top: 0;

  /* hit area: oversize the interactive area (and offset the icon within) */
  height: calc(var(--hit-area-ratio) * var(--icon-size));
  padding-bottom: calc(var(--hit-area-offset) * var(--icon-size));
  padding-left: calc(var(--hit-area-offset) * var(--icon-size));
  width: calc(var(--hit-area-ratio) * var(--icon-size));

  /* focus ring: position & alignment */
  outline: none;

  &::after {
    border-radius: 50%;
    bottom: calc(var(--hit-area-offset) * var(--icon-size) + var(--icon-size));
    content: "";
    left: calc(var(--hit-area-offset) * var(--icon-size) + var(--icon-size));
    outline: 2px solid blue;
    opacity: 0;
    pointer-events: none;
    position: absolute;
    transform: translate3d(-50%, 50%, 0);

  /* focus ring: visible on focus */
  &:focus-visible::after {
    opacity: 1;

Note: this is valid CSS now because CSS nesting is now widely available!

And of course, this extra work in execution is exactly the reason why most things lack this level of polish: time. But once you start considering how a UI element’s interactive properties (hit area + focus area) can be separated from its basic rendering, you begin to unlock new levels of perfection in UI interactions, better accessibility, and new levels of delight.

Further Reading

While this approach has been in my work for years, it didn’t occur to me to write it down until I read Matthew Ström’s post The polish paradox: the more you polish, the less you see. While Matthew’s post is less pragmatic and more a general overview, it’s a fantastic, evergreen summary of the overarching principles of UI interaction I’ll be referring to and linking to for years. It’s a perfect example about the highest levels of thoughtful interactions that many people new to frontend don’t even realize happen even in websites they frequent weekly (with my favorite example being Amazon’s hover menu tunnel which is mentioned in the post).