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.

Solution

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).