Skip to content

low-contrast: a stepper's resting state is advisory, a parked reveal is not at rest - #1148

Merged
pbakaus merged 12 commits into
mainfrom
corpus/round13-resting
Oct 23, 2026
Merged

pbakaus merged 12 commits into
mainfrom
corpus/round13-resting

Conversation

@pbakaus

@pbakaus pbakaus commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Round 13 of the detector corpus, branch E: low-contrast on text a visitor sees in another state. Under taste call r13-t1, a stepper item dimmed beside the active one is advisory; text a script parked at the start of a reveal is not at rest and is scored at its declared colour.

What the row was

On the craft run (1009), low-contrast scored four shapes of dimmed text as if the page sat that way:

  • arc.net's feature stepper: every article.feature sits at opacity 0.3 by stylesheet with transition: opacity .3s, and the active one, article.feature.is-active, is at 1. The visitor scrolls the active slot along, so each item is read lit. 21 findings, all marked false positive.
  • ramp.com: a card's copy held at inline opacity: max(.2, var(--card-animation-1)), a value a scroll script scrubs from 0.2 upward. The box is a sole child. 4 findings, all false positive.
  • raycast.com: every key of a keyboard parked at inline opacity: 0.2 with an opacity transition; the same component further down the page carries one more class (glowing) at 1. 10 findings in the pixel pass.
  • framer.com: a card parked at its start values, inline will-change: transform; opacity: 0.2; transform: translateX(-19.99px). 2 findings in the pixel pass.

The fix

All in the shared element colour pass, crates/core/src/browser/element_checks.rs.

A state (arc, advisory). dimmed_by_a_state: a box between the text and its surface below opacity 0.5, with an opacity transition, beside a same-tag sibling at opacity 1 whose class list is the dimmed box's plus exactly one token. The finding keeps its place and becomes advisory, like the decorative path. The pixel pass marks such an item's candidate advisory the same way, so a stepper over a photo stays advisory there too. It stands down when the dimmed box is a control or the text reaches it through one: button, field, tab, a[href], aria-selected, link/option/menu/tree roles, and a short box whose computed cursor is pointer where its parent's is not.

A reveal (ramp, raycast, framer; not at rest). parked_inline_for_a_reveal, read where the pass decides whether a fade is at rest. The box needs an inline opacity, and one of:

  • framer: inline transform values that displace the box, plus a will-change naming opacity or a transform. All four signals together; a stylesheet value, opacity alone, or no displacement stays at rest.
  • ramp: opacity below 0.5, an opacity transition, and a scrubbed value: var( inside max(, min(, clamp( or calc(, or a custom property set in a style attribute on the box or an ancestor.
  • raycast: opacity below 0.5, an opacity transition, at least three boxes of its tag and class list parked alike at the same inline opacity, and a peer elsewhere on the page at 1 whose class list is theirs plus one token, also with an opacity transition.

The ramp and raycast arms stand down when the box is a control, sits in one or holds one, including a clickable child with a short label and no role. The style attribute is read for the declaration that wins (the last, unless an earlier one is !important), and custom property names match case-sensitively.

An inline-parked box with a bright sibling is a state, not a reveal: it goes advisory, never dropped.

The snapshot capture now reads computed cursor (STYLE_PROPS, docs/CLI-CONTRACT.md). Stored corpus captures predate that and read it as empty, so the replay cannot exercise the cursor guard; the fixture and browser test do.

The static engine reads no transition, transform or will-change, so its output does not change. The new fixture's static goldens pin that.

Accepted as advisory

Paul accepted two shapes that match arc's exactly and become advisory with it: a checkout stepper's future steps beside the current one, and a footer list whose current item is at 1 and the rest at 0.4 with a hover transition. Nothing in a snapshot separates "the visitor moves the active slot" from "this is how the page sits". The fixture pins the checkout stepper as advisory. Footer links all faded alike, or list items with no class, still warn.

Run 1009 (craft), against main's replay

Only low-contrast moves: 1112 on main, 1108 here. 4 removed, 0 added, 21 now advisory.

Verdict Removed Now advisory
Legit 0 0
Harmless 0 0
False positive 4 (ramp 10134263, 10134264, 10134402, 10134403) 21 (arc 10133793-99, 10133824-30, 10133857-63)
Unclear 0 0

No legit mark is removed or demoted. arc.net's mobile captures do not move: that grey timeline copy is at full opacity.

framer (10133521-22) and raycast (10134381-84, 10134970-75) are pixel-pass findings, which a replay cannot run. On the stored snapshots both are caught mid reveal, so the pixel pass skips them: raycast's parked keyboard (76 of 152 desktop primaries, which holds every finding rect, and 4 of 4 on mobile) and both framer headings. They will show on the next cloud craft run.

Corpus runs, against main's replay

Run Captures Violations (main / branch) Demoted harmful (main / branch) Rule-line changes
1006 243 16 / 16 0 / 0 none
1004 177 8 / 8 0 / 0 none
47 226 14 / 14 2 / 2 none

The violations are main's own (replay against recorded), with identical id sets on both sides, so this branch loses or demotes no finding both judges called harmful. The first version demoted partner.co.il's inactive tab buttons on 1006 (10089911, 10089926, both harmful); the control guard keeps them, and 1006 now matches main.

Hook cost

The new tests sit in the shared element colour pass, which the static HTML engine also runs (color-rules). rule_cost --runs 9 over main's tests/fixtures/antipatterns plus the impeccable.style site source (803 inputs, cold per edit), main (c51e529) and this branch built from their own trees, in the order main, branch, branch, main:

Run static-html per edit, mean / p50 / p95 ms color-rules, mean / p50 / p95 ms
main, 1st 36.62 / 31.91 / 72.55 3.977 / 3.005 / 9.747
branch, 2nd 31.27 / 30.36 / 62.03 3.602 / 2.852 / 9.035
branch, 3rd 32.86 / 30.28 / 66.19 3.749 / 2.857 / 9.497
main, 4th 31.34 / 30.35 / 61.60 3.575 / 2.839 / 9.028

No difference above run-to-run noise on a loaded machine. On the hook path the reveal test reads the inline style attribute and stops at the transition check, which the static engine never passes (it reads no transitions); the state test runs only for an element that already failed contrast. The text engine is untouched.

Tests

  • Fixture tests/fixtures/antipatterns/low-contrast-resting-states.html. Should flag: the arc stepper and a checkout stepper (both advisory), footer links faded alike, tabs with and without a transition, an inline number, a lone parked badge with a lit namesake in another section, parked nav links, an inline design token (opacity: var(--muted)), a faint modifier on the dimmed box, a clickable plan option with no role, identical class lists. Should pass (URL engine): the framer, ramp and raycast shapes.
  • Unit tests in element_checks.rs for each arm and its twins, and crates/browser/tests/transient_opacity.rs, which asserts on computed colours and the advisory flag only, so it does not depend on host fonts.
  • Oracle: new cases detect-fixture-json-low-contrast-resting-states-html and detect-fixture-text-low-contrast-resting-states-html (static engine: fourteen warnings). No existing golden moved; the directory sweeps gain the same findings but are accepted deltas and not compared (DELTAS entry).
  • Review round (d39ef14): twins for the pixel-pass advisory, a clickable child, declaration order and !important, and custom property case. Runs 1009, 1006, 1004 and 47 re-measured on this head: identical to the numbers above.
  • On the merged head (05a180e, main ea69c18 merged in): cargo build --release -p impeccable, cargo xtask bundle --check, node oracle, cargo test --workspace, bun run build (no tracked changes), bun run test with this binary. All green.

AI assistance: prepared by Claude Code, under instructions from @pbakaus.

🤖 Generated with Claude Code


Note

Medium Risk
Changes when low-contrast fires and its severity across element and pixel passes; behavior is nuanced but heavily fixture-tested.

Overview
Refines low-contrast so dimmed text is not always scored as if the page permanently sits that way.

Resting UI state (advisory): Text behind a faded ancestor that has an opacity transition and a same-component sibling at full opacity (e.g. inactive stepper/tab beside the active one) still reports low-contrast but is advisory, with the same treatment in the pixel pass. Controls on the path (tabs, links, cursor: pointer chips, etc.) keep full severity.

Parked reveals (not at rest): Fades driven by inline style at animation start are excluded from “at rest” blending—Framer-style transform+will-change, scroll-scrubbed opacity: max(.2, var(...)), and groups of keys parked alike when a lit peer exists elsewhere on the page.

Snapshot capture now records computed cursor so pointer-based control detection works in the browser engine. Fixture, unit, and URL-engine tests cover the matrix.

Reviewed by Cursor Bugbot for commit 42bd9dc. Bugbot is set up for automated code reviews on this repo. Configure here.

pbakaus and others added 3 commits October 22, 2026 15:38
Text dimmed by a stepper's inactive state (a box under 0.5 opacity with an
opacity transition beside a sibling of its component at 1) reports as
advisory, unless the dimmed box or the text's own box is a control
(taste call r13-t1; arc.net). A box a script parked in its style attribute
at the start of a reveal is not at rest: opacity with a transform and
will-change (framer.com), or under 0.5 with an opacity transition and
either a var() opacity (ramp.com) or the same component revealed elsewhere
on the page (raycast.com).

Co-Authored-By: Claude Code <noreply@anthropic.com>
The reveal arm stands down for a control and needs a set of boxes parked
alike before a lit namesake elsewhere counts; the scrubbed arm needs the
variable inside a math function or set in a style attribute, so an inline
design token keeps its finding. A state needs the bright sibling to carry
the dimmed box's classes plus exactly one, and a box that sets
cursor: pointer around a short label counts as a control (the capture now
records cursor). The review's probes are should-flag rows in the fixture.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
@impeccable-reviews

impeccable-reviews Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

Retrigger
5 of 5 5/5 · Ready: no major issues found
This push leaves nothing to fix.

Nothing to fix.

About this review
  • Not sent. low-contrast detector pass isn’t a page to walk.
  • Design system. This pull request adds --muted, --Muted, --delay; it is judged against the system as it leaves it.
  • crates/live/assets/detect-antipatterns-browser.js is too large to read.
  • Read from the code alone; no page was opened.

Reviewed 1 file on 23 Oct 2026, 22:05 UTC · Run page

Reviews (10) · Last reviewed commit: Merge origin/main into corpus/round13-resting

@greptile-apps

greptile-apps Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 4/5

[Medium impact] The change reaches browser-based low-contrast findings across scanned pages.

Fix the shadow-tree control check before merging so slotted button labels keep their contrast warnings.

Fix All in CodexFindings

  1. P1 Slotted labels lose contrast warnings ▶

Summary

This PR makes dimmed stepper text advisory and treats supported inline reveal shapes as temporary states.

  • Dimmed stepper text keeps its finding but becomes advisory.
  • Text in a parked reveal is scored at its declared color.

Earlier fixes cover pointer-only children, visual advice, declaration order and custom-property case, summary, menuitemcheckbox, and hidden bright siblings. pbakaus accepted the repeated page-walk cost because the checks narrowly limit when it runs and the static hook never reaches it. pbakaus also accepted deferring the control-above-faded-box gap because no current corpus or craft replay contains that shape.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Text under a faded box] --> B{Recognized inline reveal?}
  B -->|Yes| C[Ignore fade in colour check]
  B -->|Yes| D[Skip visual check]
  B -->|No| E{Matching bright sibling and no control?}
  E -->|Yes| F[Keep contrast finding as advice]
  E -->|No| G[Keep normal contrast warning]
Loading

Reviews (10) · Last reviewed commit: "Merge origin/main into corpus/round13-re..." · Reviewed by Greptile

Comment thread crates/core/src/browser/element_checks.rs Outdated
Comment thread crates/core/src/browser/element_checks.rs
Comment thread crates/core/src/browser/element_checks.rs Outdated
Comment thread crates/core/src/browser/element_checks.rs
pbakaus and others added 3 commits October 23, 2026 09:55
The pixel pass marks a stepper item dimmed by its state as advisory,
like the element pass. A box that holds a clickable child with a short
label and no role is not parked for a reveal. The inline style reader
takes the declaration that wins (last, unless an earlier one is
!important) and matches custom property names case-sensitively.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
The colour check now treats text as decorative when it is dimmed by a
resting state (this branch) or sits under a box labelled as a screenshot
(main); both contract entries kept; bundle regenerated.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment thread crates/core/src/browser/element_checks.rs

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 30b0e03. Configure here.

Comment thread crates/core/src/browser/element_checks.rs
pbakaus and others added 2 commits October 23, 2026 12:37
…ire a shown lit peer

A disclosure's summary and a menuitemcheckbox keep their verdict like
the other controls, before the state and reveal exceptions. A revealed
peer on the page counts only when it is painted and no ancestor fades it.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment thread crates/core/src/browser/element_checks.rs
pbakaus and others added 2 commits October 23, 2026 13:23
A stepper item's active sibling that is display: none or hidden shows
no state a reader meets, so the dimmed item keeps its warning.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment thread crates/core/src/browser/element_checks.rs
pbakaus and others added 2 commits October 23, 2026 14:03
Co-Authored-By: Claude Code <noreply@anthropic.com>
Takes in #1155; bundle regenerated.

Co-Authored-By: Claude Code <noreply@anthropic.com>
Comment thread crates/core/src/browser/element_checks.rs
@pbakaus
pbakaus merged commit 2bb6563 into main Oct 23, 2026
20 checks passed
@linear-code

linear-code Bot commented Oct 23, 2026

Copy link
Copy Markdown

REN-1219

abdulwahabone added a commit that referenced this pull request Oct 24, 2026
Main moved again while the previous merge was verified: release 4.7.1
and engine 0.1.17 (#1159, #1158), the row-stripe side tab and its
follow-ups (#1133, #1156), the stepper resting state (#1148), the
corpus round 11 deferred edge cases (#1155), the contrast grounds work
that drops unmeasured handed verdicts (#1147), and the generated
provider sync. No source conflicts; the in-page bundle conflicted as a
generated file and was regenerated from the merged source with
`cargo xtask bundle`. The sweep goldens stay main's as they stand: main
carries its newest fixtures' entries in them as accepted deltas in
DELTAS.md rather than re-recording, and this branch's sweep entries are
covered by its existing DELTAS entries the same way, as in the previous
merge.

Prepared with AI assistance (Claude Code).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@pbakaus pbakaus mentioned this pull request Oct 26, 2026
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