# A focus ring that was defined, valid-looking and computed to nothing

A focus ring is only real if it computes. Connect's styling is 56 ordered layers behind one `@import` index, so a rule that reads correctly in the file it lives in can be beaten by a same-named custom property defined in a layer loaded later, or by a theme block whose selector outranks `:root`. The check that settles it is `getComputedStyle` on the real element while it really holds focus.

- **Status:** Available
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/research/focus-rings-that-computed-to-none/

## Five ways a defined ring disappears without an error

None of these produce a warning, a console message or a failing lint rule. Each leaves a declaration in the source that a reviewer reads as working, and each ends with a keyboard user unable to see where they are.

| Cause | What the source looks like | What the browser computes |
|---|---|---|
| The custom property is undefined at that element | `outline: 2px solid var(--focus-ring)` | Invalid at computed-value time — the declaration falls back to inherited or initial, which is usually no outline at all |
| A later layer redefines the same token | Two definitions, each correct in its own file | The one later in the `@import` order wins, silently, with no duplicate warning |
| A theme block outranks the token's home | `:root { --focus-ring: … }` | An attribute selector on the root element is more specific and wins regardless of which file loaded last |
| The outline is suppressed and its replacement overridden | `outline: none` plus a `box-shadow` ring | No outline, and a shadow a later rule replaced — two correct-looking rules cancelling |
| The colour resolves to the background | `outline-color: currentColor` | A ring drawn in a colour that cannot be seen against what is behind it |

The third row survives review longest, because both definitions are right. Specificity is not load order: an attribute selector on the root element beats a bare `:root` no matter which stylesheet arrived first, so moving the file changes nothing and the next person moves it back.

## The method: measure the element, not the file

1. Reach the element the way a keyboard user does — Tab to it. Do not click it, and do not add the focus class by hand.
   - Result: You are looking at the state that actually ships. A click and a Tab do not produce the same `:focus-visible` match, and a ring that fails for only one of them is the ordinary case rather than the exotic one.
2. Read the computed values off the live element: `outline-width`, `outline-style`, `outline-color` and `box-shadow` from `getComputedStyle(el)`, and the token itself from `getComputedStyle(el).getPropertyValue('--focus-ring')`.
   - Result: An empty string for the token beside a zero outline width names the failure exactly. The source cannot: it shows a declaration that was never applied.
3. Repeat in the other theme, on the same element, in the same tab.
   - Result: A token that resolves under one theme and not the other is the specificity case above, and you have just proved which one it is.
4. Record the finding against the element's `data-ui` name.
   - Result: Every visible element in `webapp/src/` carries one, so the finding stays addressable without a brittle descendant selector that the next refactor invalidates.

> **Note** This is a browser measurement, so it belongs to whichever audience is looking. Owner and customer sessions render the same components, but not always the same screen state — a check performed in one session is evidence about one session.

## Why the layer count is the cause and not the villain

Fifty-six ordered layers is a deliberate structure rather than accumulated mess: `FRONTEND-MAP.md` says which file owns a route, an element and a style layer, and the ordering is what lets a component's own rules sit above the resets and below the theme. The cost of that structure is precisely this class of defect — a token can be defined in more than one honest place, and only the cascade knows which one won.

Collapsing the layers would trade a silent override for a merge conflict on every change to shared styling, which is the worse trade in a codebase with no build step. The cheaper answer is to keep the layers and stop treating the source as evidence.

## What the measurement does not tell you

- It proves the ring computes. It does not prove the ring is *visible*: contrast against whatever sits behind it is a separate measurement with a separate threshold.
- It covers the elements you tabbed to. Anything that only appears inside an open menu, a modal or an error state was not measured, and those are exactly where overrides concentrate.
- It says nothing about elements that never take focus — a control that cannot be reached by keyboard has no ring to compute.
- There is no figure here for how many elements across the application were affected: UNKNOWN. The method finds them one at a time.

## Questions

### Why not assert the CSS in a test instead of opening a browser?

Because a test that parses the stylesheet is asserting against the same source that already looked correct. An assertion is only worth having if it would fail when the requirement is unmet, and a source-reading assertion passes happily while the ring computes to nothing. To put this in a suite, drive a real browser and read computed style there.

### Does `!important` fix it?

It moves the problem. An `!important` on the focus rule wins today and makes the next legitimate override impossible, so the following defect is a ring that cannot be themed. The fix for a specificity collision is one definition of the token, not a louder one.

### How would this have been caught earlier?

By looking at the changed screen in a browser before the commit, in each audience's session. Defects of this shape are invisible in a diff and obvious in half a minute of use — the same lesson as [Defects that are invisible in the source](/research/invisible-ui-defects/).

## Related

- [Defects that are invisible in the source and obvious in a browser](https://connectbyjbrh.com/research/invisible-ui-defects/)
- [Five ways a test suite has passed while proving nothing](https://connectbyjbrh.com/research/false-passing-tests/)
- [One implementation, two audiences](https://connectbyjbrh.com/research/two-audiences-one-implementation/)
- [Technology reference](https://connectbyjbrh.com/docs/technology/)
- [Writing documentation that both a person and a retrieval system can use](https://connectbyjbrh.com/research/writing-for-people-and-machines/)

## What this page is based on

- `docs-source/sources/GENERAL.md` §4 — the frontend: 56 ordered stylesheet layers behind one `@import` index, `data-ui` names, `FRONTEND-MAP.md`
- `docs-source/facts.py` — `MEASURED.style_layers`
- Connect documentation plan — the page brief for this note (`docs-source/assignments/batch-research_c.json`)
