Skip to content

refactor(effect-schema-form-aria): move the focus ring from boxShadow to outline - #1185

Closed
schickling-assistant wants to merge 1 commit into
schickling-assistant/2026-09-02-gate-project-pluginsfrom
schickling-assistant/2026-09-02-focus-ring-outline
Closed

refactor(effect-schema-form-aria): move the focus ring from boxShadow to outline#1185
schickling-assistant wants to merge 1 commit into
schickling-assistant/2026-09-02-gate-project-pluginsfrom
schickling-assistant/2026-09-02-focus-ring-outline

Conversation

@schickling-assistant

@schickling-assistant schickling-assistant commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Top of stack #1177, on #1184. Moves the focus ring from boxShadow to outline at all four ring sites in the package.

⚠ This needs a human decision

The conversion is not pixel-equivalent. Accepting the convention means accepting a small visible change to the focus ring on every field in the package. That is a design call, not mine, so this PR stays draft until someone makes it.

The measurement

The FocusRing stories added in #1183 caught this on their first use — which is the entire argument for having landed them first.

Captured before and after on one host. Self-comparison control at 0 differing pixels. Canvas dimensions identical in every case, so this is paint, not layout:

story differing max channel Δ
text / number / literal-trigger 3856 / 73920 px (5.216%) 83
optional-number (compact input) 351 / 61440 px (0.571%) 91

The bounding box covers the whole ring perimeter, not the corners — so the band moves. This is not anti-aliasing noise.

box-shadow: 0 0 0 1px C and outline: 1px solid C are therefore not interchangeable, which contradicts what I first wrote in these files. That comment is corrected in place, at the site, with the numbers.

I have not tuned it back to byte-identical. Matching a box-shadow's exact band with outline-offset would trade a named visible delta for an unexplained coincidence, and the second is worse.

Why do it at all

StyleX writes one value per property. A ring and a decorative shadow are two writers of boxShadow, and when they meet one silently wins — no compile error, nothing for a type checker to see. That composed-shorthand collision is a confirmed defect class in this migration: the design system had 8 genuine same-element collisions across 9 files where the two coexisted.

LiteralField already carries a real elevation shadow on boxShadow for its popover. Different element today — but a trigger is exactly the control that later grows one, and that is the pair that would have collided silently.

One site called out in place

NumberField's compact input is the only ring keyed on a pseudo-class rather than an attribute, which makes it the site exposed to the upstream @stylexjs/shared@0.19.0 priority-table defect: pseudo-classes are spelled camelCase in that table, so :focus-visible falls through to the unknown-pseudo default of 40 while :hover sits at 130.

Nothing writes :hover on outline there, so the two cannot meet — and keeping the ring on its own property is what guarantees that, rather than luck.

Verified

  • Cold composite build: tsc --build in a fresh worktree with no dist exits 0. That is the configuration CI has, and the one a warm tree cannot see past.
  • Zero boxShadow focus sites remain; both surviving boxShadow uses are real elevation shadows.
  • The comparator asserts equal dimensions and a non-empty file set rather than reporting a number, and its self-comparison control runs on every invocation.

What is not claimed

That the new ring looks better, or that 5.2% is acceptable. Only that it is 5.2%, that it is paint rather than layout, and that nothing else in these files changed.

Posted on behalf of @schickling
field value
agent_identity dev3.direct.omp.mhx73ur2
session dev3.mhx73ur2
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.0.11
agent_runtime OMP 18.0.11
tooling_profile dotfiles@e8f0615-dirty

… to outline

Reserves 'outline' for the focus ring at all four ring sites, so a ring and a
decorative shadow can coexist: StyleX writes one value per property, and a ring
plus a shadow are two writers of boxShadow with no compile error when one
silently wins. LiteralField already has a real elevation shadow on boxShadow for
its popover -- a different element today, but a trigger is exactly the control
that later grows one.

VISIBLE DELTA, MEASURED -- this conversion is NOT pixel-equivalent, and the
FocusRing stories added in #1183 are what caught it on their first use.

Captured before and after on one host, with a self-comparison control at zero
and identical canvas dimensions in every case, so this is paint and not layout:

  text / number / literal-trigger   3856/73920 px (5.216%)  maxChannelDelta 83
  optional-number (compact input)     351/61440 px (0.571%)  maxChannelDelta 91

The bounding box covers the whole ring perimeter rather than the corners, so the
band moves -- this is not anti-aliasing noise. 'box-shadow: 0 0 0 1px' and
'outline: 1px solid' are therefore not interchangeable, which contradicts the
comment I first wrote here and which is now corrected in place at the site.

THIS NEEDS A HUMAN DECISION and the PR says so: accepting the convention means
accepting a small visible change to the focus ring on every field in the
package. I have not tried to tune it back to byte-identical, because matching a
box-shadow's exact band with outline-offset would trade a named visible delta
for an unexplained coincidence.

One site is called out in place: NumberField's compact input is the only ring
keyed on a pseudo-class rather than an attribute, which makes it the site
exposed to the upstream @stylexjs/shared priority-table defect where
':focus-visible' falls through to 40 against ':hover' at 130. Nothing writes
':hover' on outline there, so the two cannot meet -- and keeping the ring on its
own property is what guarantees that rather than luck.

VERIFIED: cold composite build ('tsc --build' in a fresh worktree with no dist)
exits 0; zero boxShadow focus sites remain; both remaining boxShadow uses are
real elevation shadows.
@schickling-assistant schickling-assistant changed the title schickling assistant/2026 09 02 focus ring outline refactor(effect-schema-form-aria): move the focus ring from boxShadow to outline Sep 2, 2026
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Storybook Previews

Subject Status Report Details Updated
react-inspector success react-inspector preview deployed Preview is ready 2026-09-02 09:39 UTC
effect-react success effect-react preview deployed Preview is ready 2026-09-02 09:39 UTC
notion-cli success notion-cli preview deployed Preview is ready 2026-09-02 09:39 UTC
genie success genie preview deployed Preview is ready 2026-09-02 09:39 UTC
notion-md success notion-md preview deployed Preview is ready 2026-09-02 09:39 UTC
tui-react success tui-react preview deployed Preview is ready 2026-09-02 09:39 UTC
effect-schema-form-aria success effect-schema-form-aria preview deployed Preview is ready 2026-09-02 09:39 UTC
megarepo success megarepo preview deployed Preview is ready 2026-09-02 09:39 UTC
notion-react success notion-react preview deployed Preview is ready 2026-09-02 09:39 UTC
Report history

PR 1185 · 2026-09-02 09:40 UTC

Subject Status Report Details Updated
react-inspector success react-inspector preview deployed Preview is ready 2026-09-02 09:39 UTC
effect-react success effect-react preview deployed Preview is ready 2026-09-02 09:39 UTC
notion-cli success notion-cli preview deployed Preview is ready 2026-09-02 09:39 UTC
genie success genie preview deployed Preview is ready 2026-09-02 09:39 UTC
notion-md success notion-md preview deployed Preview is ready 2026-09-02 09:39 UTC
tui-react success tui-react preview deployed Preview is ready 2026-09-02 09:39 UTC
effect-schema-form-aria success effect-schema-form-aria preview deployed Preview is ready 2026-09-02 09:39 UTC
megarepo success megarepo preview deployed Preview is ready 2026-09-02 09:39 UTC
notion-react success notion-react preview deployed Preview is ready 2026-09-02 09:39 UTC

schickling-assistant added a commit that referenced this pull request Sep 2, 2026
Caught by accident and worth recording as a trap: PR #1185's branch was forked
from #1183's branch rather than from #1184's, so it never contained #1184. The
stack still LOOKED right, because 'gh stack link' set #1185's base to #1184 and
GitHub computes a PR diff from the merge-base -- which was #1183's tip, so
#1185's diff was exactly its own commit and read as correctly stacked.

Setting a base does not make a branch contain that base. A clean-looking PR diff
is equally consistent with 'correctly stacked' and 'forked from a sibling', and
only ancestry distinguishes them: 'git merge-base --is-ancestor' over all eight
heads found seven IN and one MISSING.

It surfaced because a liveness probe tried to back up
gate/project.unit.test.ts, the backup failed silently, and the file turned out
to be untracked -- so a broken 'cp' in a throwaway check is what revealed a
missing PR.

The conflict was cleanly disjoint and both sides were needed: this branch
carries the React alias/pin work (reactAliasRules, pinReactToConsumer) that
makes cross-checkout consumers render at all, and #1184 carries the plugins
option and the storybookPluginFor seam. Resolved by taking this side and
re-applying #1184's threading, with the React pin kept FIRST so its alias
applies to whatever a caller's transform emits.

VERIFIED: both feature sets present, zero markers, and the plugin-placement
unit test passes 4/4 in the merged tree -- the same test that fails 3 of 4 when
the placement is reverted.
@schickling-assistant

Copy link
Copy Markdown
Collaborator Author

Superseded by #1191, which collapses this stack onto main as one tree.

Closing rather than merging: propagating main through eight stacked branches meant re-reconciling the lockfile, the fixed-output hashes and the Buck projection once per branch. Branch 2 alone produced 11 conflicts, a semantically broken auto-merged lockfile, a BUCK admission rename and a circular tooling block. On one tree each of those happens once.

This PR's content is in #1191, verified with git merge-base --is-ancestor over all eight heads — #1184 was found missing that way and merged in, because a gh stack base does not make a branch contain that base.

This body stays as the record of the per-change evidence, which #1191 summarises but does not reproduce in full.

Posted on behalf of @schickling
field value
agent_identity dev3.direct.omp.mhx73ur2
session dev3.mhx73ur2
agent_persona generalist
agent_supervisor unavailable
agent_tool OMP
agent_tool_version 18.0.11
agent_runtime OMP 18.0.11
tooling_profile dotfiles@e8f0615-dirty

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant