disabled and aria-disabled Elements from Contrast Tests 107 & 109Bug (ENG-1723): Access Engine color-contrast tests 107 (normal-size text, WCAG 1.4.3, requires 4.5:1) and 109 (large-scale text, WCAG 1.4.3 large-text path, requires 3:1) were flagging low-contrast text on elements that are natively disabled or carry aria-disabled="true". Per WCAG, text and visual information in an inactive (disabled) user-interface component has no contrast requirement, so these were false positives.
Impact: Greyed-out controls (which are intentionally low-contrast to signal that they are unavailable) generated noise. Real, fixable violations were buried under false positives on buttons, menu items, and tabs that the author legitimately disabled.
:not([disabled]):not([aria-disabled="true"])) to the candidate selector. An element is exempt only when it itself carries a literal disabled attribute or aria-disabled="true". There is no ancestor walk.<fieldset disabled> descendants: a disabled fieldset makes its form-control descendants functionally disabled per the HTML spec, but those descendants do not carry a disabled attribute of their own. Because the selector is a literal attribute check, :not([disabled]) still matches them, so they are still evaluated for contrast. WCAG would treat them as exempt; the engine does not. Extending the exemption to inherited-disabled descendants would require an ancestor walk and is out of scope for this ticket.aria-disabled="true" marks only that element as disabled; it does not disable or exempt descendant elements in the accessibility tree. An element is exempt via aria-disabled only when it carries the attribute itself — this matches both the WCAG spec and the engine's element-level selector.aria-disabled="false" (or absence of the attribute) does not exempt — those elements are active and still tested (regression guard).aria-disabled tabs in a tablist where a tab is temporarily unavailable.disabled attribute or aria-disabled="true". The check is element-level only — it does not walk ancestors, so it does not propagate the exemption to descendants of a disabled container.
:not([disabled]):not([aria-disabled="true"])
<fieldset disabled> that do not carry their own disabled attribute are not exempted — they are still evaluated (see the "Known Limitations" section below). Extending the exemption to inherited-disabled fieldset descendants would require an explicit ancestor walk and is out of scope for this ticket.
aria-disabled="false" or no disabled state) continue through to the normal 4.5:1 / 3:1 evaluation.
display:none) — id="na-hidden-disabled"(The disabled low-contrast button above is hidden via display:none.)
Hidden elements are excluded from contrast evaluation by visibility filtering before 107/109 even run. Should not be flagged regardless of the disabled fix.
visibility:hidden — id="na-vishidden-disabled"Not rendered to the user; outside the scope of tests 107/109. Should not be flagged.
id="na-good-enabled"Active paragraph text at #222 on #fff (~16:1). This is normal-size text well above the 4.5:1 threshold of test 107.
Contrast already passes; nothing for 107 or 109 to report. Not affected by the disabled exemption. Should not be flagged.
id="na-good-disabled"Disabled AND high-contrast — neither the old behavior nor the new exemption would flag it. N/A to the fix. Should not be flagged.
<button disabled> with low-contrast label — id="pass-button-disabled"Element is natively disabled. Test 107 must skip it — disabled UI components are exempt from contrast. Should not be flagged.
<input disabled> with low-contrast value — id="pass-input-disabled"Disabled input value text is exempt. Test 107 must skip it. Should not be flagged.
aria-disabled="true" and low-contrast text — id="pass-ariadisabled"The element itself carries aria-disabled="true", marking this component inactive, so the WCAG exemption applies to this element. Test 107 must skip it. Should not be flagged.
aria-disabled="true" heading-style text (test 109 path) — id="pass-ariadisabled-large"Large text routes through test 109 (3:1 threshold) but the element is aria-disabled="true", so 109 must skip it too. Should not be flagged.
id="pass-enabled-good"The enabled button passes contrast; the disabled one is exempt. Neither should be flagged. Confirms the fix does not over-skip enabled siblings.
id="fail-enabled-button"Active control, normal-size text, ≈ 1.96:1 (< 2:1) < 4.5:1. Test 107 must still flag this. Regression guard: the fix must not suppress enabled controls.
id="fail-enabled-large"Active large low-contrast heading text (24px bold)
Active large text, ≈ 1.96:1 (< 2:1) < 3:1. Test 109 must still flag this. Regression guard for the large-text path.
aria-disabled="false" and low contrast — id="fail-aria-false"aria-disabled="false" means the component is active, so the exemption does NOT apply. Test 107 must still flag this. Guards against matching the attribute name rather than its value.
aria-disabled="false" container — id="fail-aria-false-ancestor"The span carries no disabled state of its own, and the only ancestor is aria-disabled="false" (active). The element-level selector matches the span, so test 107 must still flag it. Guards against matching aria-disabled by attribute presence rather than its "true" value.
id="fail-fake-disabled"Visually greyed but has neither disabled nor aria-disabled="true" — it is operable. Test 107 must still flag it. The exemption is driven by state, not by appearance.
aria-disabled="true" ancestor — id="fail-ancestor-aria"Only the ancestor <div> carries aria-disabled="true"; the <span> does not. Per WCAG, aria-disabled does not propagate to descendants, so the span is active, low-contrast text. The element-level selector matches it and test 107 must still flag it — this is a genuine true positive, not a missed exemption. The outer <div> itself is exempt (it carries the attribute), but the exemption stops there.
id="bug-disabled-button"Pre-fix: flagged by test 107 ❌ (false positive — engine ignored the disabled state). Post-fix: not flagged ✓ (disabled UI is exempt from contrast).
id="bug-ariadisabled-text"Pre-fix: flagged by test 107 ❌ (engine treated it as active text). Post-fix: not flagged ✓ (aria-disabled="true" exempts it).
id="bug-disabled-large"Disabled large low-contrast text (109 path)
Pre-fix: flagged by test 109 ❌. Post-fix: not flagged ✓ (large text in a disabled component is also exempt).
The fix exempts an element only when that element carries disabled or aria-disabled="true" (CSS :not([disabled]):not([aria-disabled="true"])); it does not walk ancestors. The cases below are therefore still flagged after the fix. For <fieldset disabled> descendants, WCAG would treat the text as exempt (native disabled inherits to form controls), so this is a documented engine limitation rather than a fixed case. Extending the exemption would require an ancestor walk (separate ticket).
<fieldset disabled> descendant controls — id="limitation-fieldset-disabled"The <fieldset> itself carries disabled and is exempt, but none of its descendants (<legend>, <label>, <input>, <button>) carry a disabled attribute of their own. The element-level :not([disabled]) selector still matches each one, so test 107 still flags their low-contrast text (all are normal-size, so 109 does not apply). The genuine limitation is the <input> and <button>: HTML disabled propagates only to form-associated descendants, so those two are functionally disabled and WCAG would consider their text exempt — yet the engine still flags them. The <legend> and <label> are not form controls and are not disabled, so flagging their low-contrast text is correct per WCAG. Not addressed by ENG-1723.
fieldset[disabled] — id="limitation-fieldset-child"The <input> has no disabled attribute of its own — only its <fieldset> ancestor does. The element-level selector matches the input, so test 107 still flags it post-fix. Same root cause as above; not fixed by ENG-1723.
id="rw-submit"Classic UX pattern: the Submit button is disabled and greyed out until validation passes. Test 107 must skip it. Should not be flagged.
id="rw-nav"The "Billing" item is aria-disabled="true" and intentionally greyed. Active items keep good contrast; the disabled item is exempt. Should not be flagged by 107.
id="rw-tab"The "Advanced" tab is aria-disabled="true" while the feature is locked. It is exempt from contrast. Active tabs keep good contrast. The disabled tab should not be flagged by 107.
id="rw-active-grey"This button has no disabled state — it is fully operable but styled too light.
A genuine contrast defect: an operable control with ≈ 1.96:1 text. Test 107 must still flag it. Demonstrates the fix does not give cover to enabled-but-light controls.