Skip to content

Focus ring is captured DOM with hand-computed geometry #384

Description

@nathanacurtis

Problem

A focus ring captured as a Figma layer becomes a real DOM element with hand-computed geometry. In deButton:

<span className="de-button__focus-ring" data-element="focusRing"></span>

with CSS that pins its size per variant:

.de-button__focus-ring { height: 36px; border-radius: 8px; }
.de-button[data-size="Medium"]:focus-visible .de-button__focus-ring { height: 44px; }

The button is 28px and the ring is 36px — the difference is a constant someone measured, not a relationship. Three consequences:

  • Retheme the button's height and the ring stops matching
  • Add a size variant and the ring must be hand-extended to cover it
  • border-radius: 8px is hardcoded while the root's radius is a token, so retheming the corner breaks the pairing

There is also an empty element in the markup that draws nothing on its own, and (until recently) it was a <div> inside a <button> — invalid content model.

Why this is filed rather than fixed

The obvious fix — have the transform recognise a focus-ring layer and emit CSS instead — is pattern inference, which this pipeline has been deliberately removing. It would mean guessing from an element's name or its styling that it is "a focus ring," which is exactly the class of heuristic that produced the glyph-mask box bug (isGlyphLike treating an instance with a name prop as a glyph) and the enum-label size parser (glyphConfigSize reading "20x20" as pixels). Both were deleted rather than relocated.

So the transform should not be taught to detect this shape.

Options

Upstream (preferred). Don't draw focus rings as layers. A focus indicator is a platform affordance, and the roles docs already recommend leaving focus unclassified and using the platform default, which meets contrast requirements and matches user expectation. This makes the whole problem disappear at the source.

Declared. If a library genuinely wants a specced ring, it should say so — an annotation, or a states classification the transform can act on without guessing. That fits the declared-not-inferred rule, but it is new vocabulary for a narrow case.

Derived geometry. Keep the element but emit inset: -4px and an inherited radius rather than per-size pixel constants. This does not need inference — it is a better emission of the same captured data — and it removes the per-variant patching even if the element stays.

The third is worth doing regardless of the first two, because it is a strict improvement with no new signal required.

Notes

Surfaced by an independent review of generated output for DE Button and DE Favorite button. Related: the @layer specs wrapping and the UA-reset block both landed already.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

clispecs-cli commandsreact-from-specsTransformer from specs into React codewebcomponents-from-specsTransformer from specs into web components code

Type

No type

Fields

Priority

Low

Projects

  • Status
    Backlog

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions