Skip to content

fix(theme): fix light theme a11y contrast and demo axe violations#3945

Open
ciiay wants to merge 2 commits into
redhat-developer:mainfrom
ciiay:rhdhbug-2243-fix-accessibility-violations
Open

fix(theme): fix light theme a11y contrast and demo axe violations#3945
ciiay wants to merge 2 commits into
redhat-developer:mainfrom
ciiay:rhdhbug-2243-fix-accessibility-violations

Conversation

@ciiay

@ciiay ciiay commented Jul 23, 2026

Copy link
Copy Markdown
Member

Description

Fixes light-theme WCAG contrast failures and demo accessibility issues so the theme Playwright axe suite can enforce violations again. Updates the RHDH light palette and bridges Backstage UI secondary text tokens, corrects demo markup/labels, and restores axe assertions with scoped disables only for known upstream issues.

Root cause

Light palette text.secondary / MUI info/warning colors failed WCAG AA on RHDH chrome; BUI --bui-fg-secondary (~#797979) failed on #f2f2f2; demos had orphan UserSettingsThemeToggle list items and unlabeled BUI controls; upstream react-aria aria-controls IDs and material-table nested interactive patterns required scoped axe disables.

Fixed

  • RHDHBUGS-2243 — Accessibility Violations Detected in Theme Plugin Light Theme

UI before changes

Before fix

UI after changes

After fix

Test Plan

  • Theme workspace yarn tsc:full, yarn build:all, yarn test --watchAll=false, yarn build:api-reports:only
  • Repro Playwright e2e (legacy app) passes with axe asserting violations
  • Review before/after recordings in this PR
  • Spot-check light theme secondary text / BUI tabs / ThemeToggle / form controls visually

Checklist

  • A changeset describing the change and affected packages. (more info)
  • Added or Updated documentation
  • Tests for new functionality and regression tests for bug fixes
  • Screenshots attached (for UI changes)

Note

This bug fix was identified and implemented using the bug-fix and raise-pr agent skills. Please verify the fix thoroughly before merging.

Made with Cursor

@ciiay
ciiay requested review from a team and logonoff as code owners July 23, 2026 18:40
@rhdh-gh-app

rhdh-gh-app Bot commented Jul 23, 2026

Copy link
Copy Markdown

Changed Packages

Package Name Package Path Changeset Bump Current Version
app-legacy workspaces/theme/packages/app-legacy none v0.0.0
app workspaces/theme/packages/app none v0.0.0
@red-hat-developer-hub/backstage-plugin-bcc-test workspaces/theme/plugins/bcc-test patch v0.5.0
@red-hat-developer-hub/backstage-plugin-bui-test workspaces/theme/plugins/bui-test patch v0.5.0
@red-hat-developer-hub/backstage-plugin-mui4-test workspaces/theme/plugins/mui4-test patch v0.5.0
@red-hat-developer-hub/backstage-plugin-mui5-test workspaces/theme/plugins/mui5-test patch v0.5.0
@red-hat-developer-hub/backstage-plugin-theme workspaces/theme/plugins/theme patch v0.15.0

@rhdh-qodo-merge

Copy link
Copy Markdown

PR Summary by Qodo

fix(theme): fix light theme WCAG contrast and demo axe violations

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

Grey Divider

AI Description

• Fixes WCAG AA contrast failures in RHDH light theme (secondary text, MUI info/warning colors) and
 bridges Backstage UI's --bui-fg-secondary token.
• Corrects demo markup: wraps UserSettingsThemeToggle in lists, adds aria-labels to unlabeled
 BUI controls, and improves color contrast in demo samples.
• Restores axe violation assertions in Playwright e2e tests, scoping disables only to known upstream
 aria-valid-attr-value and nested-interactive issues.
• Adds a changeset documenting the patch across theme and demo test packages.
Diagram

graph TD
  A["lightTheme.ts palette"] --> B["ThemeProvider.tsx"]
  C["buiTokenBridge.css"] --> B
  B --> D["Demo Pages"]
  D --> E["BCC/BUI/MUI4/MUI5 Test Components"]
  D --> F["Playwright axe e2e tests"]
  F --> G{"Upstream rule?"}
  G -->|"yes: scoped disable"| H["Pass"]
  G -->|"no: assert violations"| H
Loading
High-Level Assessment

The fix combines a palette-level correction (lightTheme.ts) with a CSS variable bridge (buiTokenBridge.css / ThemeProvider inline style) to cover both native MUI theming and the newer Backstage UI (@backstage/ui) token system, which is the correct approach given the two theming systems coexist and BUI tokens live in CSS layers that aren't reachable from the MUI theme object alone. Scoping axe rule disables to specific upstream rule IDs (rather than skipping assertions entirely) is the right tradeoff between enforcing real violations and tolerating known false positives from react-aria/material-table internals.

Files changed (19) +171 / -28

Bug fix (18) +162 / -28
custom-theme.spec.tsRe-enable axe violation assertions in legacy app e2e tests +0/-3

Re-enable axe violation assertions in legacy app e2e tests

• Removes 'skipViolationsAssert: true' overrides so accessibility scans now fail on violations.

workspaces/theme/packages/app-legacy/e2e-tests/custom-theme.spec.ts

acessibility.tsScope axe rule disables to known upstream issues +13/-0

Scope axe rule disables to known upstream issues

• Adds a documented list of upstream-only false positive rules ('aria-valid-attr-value', 'nested-interactive') and disables only those via AxeBuilder.

workspaces/theme/packages/app-legacy/e2e-tests/utils/acessibility.ts

buiTokenBridge.cssAdd CSS override for BUI secondary text token +29/-0

Add CSS override for BUI secondary text token

• New unlayered stylesheet overriding '--bui-fg-secondary' for light/dark modes to meet WCAG AA contrast against RHDH chrome.

workspaces/theme/packages/app-legacy/src/buiTokenBridge.css

index.tsxImport BUI token bridge CSS after Backstage UI styles +2/-0

Import BUI token bridge CSS after Backstage UI styles

• Loads 'buiTokenBridge.css' after '@backstage/ui/css/styles.css' so the RHDH override takes precedence.

workspaces/theme/packages/app-legacy/src/index.tsx

custom-theme.spec.tsRe-enable axe violation assertions in app e2e tests +0/-3

Re-enable axe violation assertions in app e2e tests

• Removes 'skipViolationsAssert: true' overrides so accessibility scans now fail on violations.

workspaces/theme/packages/app/e2e-tests/custom-theme.spec.ts

acessibility.tsScope axe rule disables to known upstream issues +13/-0

Scope axe rule disables to known upstream issues

• Adds a documented list of upstream-only false positive rules and disables only those via AxeBuilder.

workspaces/theme/packages/app/e2e-tests/utils/acessibility.ts

buiTokenBridge.cssAdd CSS override for BUI secondary text token +29/-0

Add CSS override for BUI secondary text token

• New unlayered stylesheet overriding '--bui-fg-secondary' for light/dark modes to meet WCAG AA contrast.

workspaces/theme/packages/app/src/buiTokenBridge.css

index.tsxImport BUI token bridge CSS after Backstage UI styles +2/-0

Import BUI token bridge CSS after Backstage UI styles

• Loads 'buiTokenBridge.css' after '@backstage/ui/css/styles.css' so the RHDH override takes precedence.

workspaces/theme/packages/app/src/index.tsx

BCCTestPage.tsxWrap ThemeToggle in list container +3/-1

Wrap ThemeToggle in list container

• Wraps 'UserSettingsThemeToggle' in a '<ul>' to avoid orphan list-item accessibility violations.

workspaces/theme/plugins/bcc-test/src/components/BCCTestPage.tsx

TableExample.tsxDisable table column drag to avoid nested-interactive violation +10/-1

Disable table column drag to avoid nested-interactive violation

• Sets 'options={{ draggable: false }}' on the Table demo to prevent nested focusable drag handles inside sort labels.

workspaces/theme/plugins/bcc-test/src/components/TableExample.tsx

BUITestPage.tsxWrap ThemeToggle in list container +5/-1

Wrap ThemeToggle in list container

• Wraps 'UserSettingsThemeToggle' in a '<ul>' inside header custom actions to fix orphan list-item violation.

workspaces/theme/plugins/bui-test/src/components/BUITestPage.tsx

FormComponents.tsxAdd aria-labels to unlabeled Checkbox/Switch demos +6/-4

Add aria-labels to unlabeled Checkbox/Switch demos

• Adds 'aria-label' attributes to unlabeled 'Checkbox' and 'Switch' demo controls to fix missing accessible name violations.

workspaces/theme/plugins/bui-test/src/components/FormComponents.tsx

MUI4TestPage.tsxWrap ThemeToggle in list container +3/-1

Wrap ThemeToggle in list container

• Wraps 'UserSettingsThemeToggle' in a '<ul>' to fix orphan list-item accessibility violation.

workspaces/theme/plugins/mui4-test/src/components/MUI4TestPage.tsx

InlineStyles.tsxImprove inline style demo contrast +10/-2

Improve inline style demo contrast

• Replaces low-contrast gray inline button styling with darker background and white text for adequate contrast.

workspaces/theme/plugins/mui5-test/src/components/InlineStyles.tsx

MUI5TestPage.tsxWrap ThemeToggle in list container +3/-1

Wrap ThemeToggle in list container

• Wraps 'UserSettingsThemeToggle' in a '<ul>' to fix orphan list-item accessibility violation.

workspaces/theme/plugins/mui5-test/src/components/MUI5TestPage.tsx

ThemeTestPage.tsxWrap ThemeToggle in list container in theme dev page +3/-1

Wrap ThemeToggle in list container in theme dev page

• Wraps 'UserSettingsThemeToggle' in a '<ul>' in the theme plugin's own dev test page.

workspaces/theme/plugins/theme/dev/ThemeTestPage.tsx

ThemeProvider.tsxInject runtime override for BUI secondary text color +22/-9

Inject runtime override for BUI secondary text color

• Adds an unlayered inline '<style>' tag setting '--bui-fg-secondary' to the theme's 'text.secondary' value, ensuring it overrides Backstage UI's layered token for both light and dark modes.

workspaces/theme/plugins/theme/src/components/ThemeProvider.tsx

lightTheme.tsFix light palette contrast for secondary text, info and warning +9/-1

Fix light palette contrast for secondary text, info and warning

• Updates 'text.secondary' to PatternFly-derived '#6A6E73' and overrides MUI 'info.main'/'warning.main' with darker shades to pass WCAG AA on white backgrounds.

workspaces/theme/plugins/theme/src/lightTheme.ts

Documentation (1) +9 / -0
amber-scroll-mend.mdAdd changeset for a11y contrast fix +9/-0

Add changeset for a11y contrast fix

• Documents the patch bump across theme, bcc-test, bui-test, mui4-test, and mui5-test packages.

workspaces/theme/.changeset/amber-scroll-mend.md

@codecov

codecov Bot commented Jul 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 56.13%. Comparing base (37c0bba) to head (3ae0230).
⚠️ Report is 15 commits behind head on main.
✅ All tests successful. No failed tests found.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3945      +/-   ##
==========================================
- Coverage   56.13%   56.13%   -0.01%     
==========================================
  Files        2382     2382              
  Lines       95555    95559       +4     
  Branches    26715    26715              
==========================================
  Hits        53640    53640              
- Misses      40235    40239       +4     
  Partials     1680     1680              
Flag Coverage Δ *Carryforward flag
adoption-insights 84.54% <ø> (ø) Carriedforward from 002f413
ai-integrations 69.26% <ø> (ø) Carriedforward from 002f413
app-defaults 69.79% <ø> (ø) Carriedforward from 002f413
augment 46.67% <ø> (ø) Carriedforward from 002f413
boost 75.89% <ø> (ø) Carriedforward from 002f413
bulk-import 72.63% <ø> (ø) Carriedforward from 002f413
cost-management 13.55% <ø> (ø) Carriedforward from 002f413
dcm 60.72% <ø> (ø) Carriedforward from 002f413
extensions 56.28% <ø> (ø) Carriedforward from 002f413
global-floating-action-button 71.18% <ø> (ø) Carriedforward from 002f413
global-header 62.19% <ø> (ø) Carriedforward from 002f413
homepage 47.67% <ø> (ø) Carriedforward from 002f413
install-dynamic-plugins 56.77% <ø> (ø) Carriedforward from 002f413
intelligent-assistant 74.05% <ø> (ø) Carriedforward from 002f413
konflux 91.98% <ø> (ø) Carriedforward from 002f413
lightspeed 69.02% <ø> (ø) Carriedforward from 002f413
mcp-integrations 83.40% <ø> (ø) Carriedforward from 002f413
orchestrator 62.65% <ø> (ø) Carriedforward from 002f413
quickstart 65.18% <ø> (ø) Carriedforward from 002f413
sandbox 79.56% <ø> (ø) Carriedforward from 002f413
scorecard 82.66% <ø> (ø) Carriedforward from 002f413
theme 82.98% <0.00%> (-0.87%) ⬇️
translations 5.12% <ø> (ø) Carriedforward from 002f413
x2a 55.05% <ø> (ø) Carriedforward from 002f413

*This pull request uses carry forward flags. Click here to find out more.


Continue to review full report in Codecov by Harness.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 37c0bba...3ae0230. Read the comment docs.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@rhdh-qodo-merge

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Context used
⚠️ Tickets: not configured — ticket URL found in PR but could not be fetched — check ticket provider credentials
✅ Compliance rules (platform): 11 rules
✅ Cross-repo context
  Explored: repo: redhat-developer/rhdh (sha: d090bd1a)
  Not relevant to this PR: redhat-developer/rhdh-chart
  Not relevant to this PR: redhat-developer/rhdh-operator
  Not relevant to this PR: redhat-developer/rhdh-local

Grey Divider


Remediation recommended

1. Axe rules disabled globally 🐞 Bug ☼ Reliability
Description
runAccessibilityTests unconditionally disables axe rules aria-valid-attr-value and
nested-interactive for every scan, so regressions for those rules in repo-owned markup across any
scanned page/tab will no longer be detected. This reduces the reliability of the accessibility gate
beyond the specific upstream components the comment references.
Code

workspaces/theme/packages/app/e2e-tests/utils/acessibility.ts[R38-42]

) {
  const accessibilityScanResults = await new AxeBuilder({ page })
    .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
+    .disableRules([...UPSTREAM_AXE_DISABLE_RULES])
    .analyze();
Relevance

⭐⭐⭐ High

Repo has precedent challenging broad axe ignores; will likely request scoping/disabling only where
needed.

PR-#2509

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The helper disables both rules on every AxeBuilder execution and is used repeatedly across multiple
pages/tabs in the e2e suite, so any violations of those rules anywhere in the scanned DOM will be
suppressed.

workspaces/theme/packages/app/e2e-tests/utils/acessibility.ts[19-55]
workspaces/theme/packages/app/e2e-tests/custom-theme.spec.ts[64-95]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`runAccessibilityTests()` disables the `aria-valid-attr-value` and `nested-interactive` rules for *all* pages scanned, which can hide new violations introduced by this repo in unrelated areas.

### Issue Context
The PR intends to suppress known upstream false positives, but the current approach disables the entire rule globally rather than scoping to the known offending nodes/pages.

### Fix Focus Areas
- workspaces/theme/packages/app/e2e-tests/utils/acessibility.ts[19-55]
- workspaces/theme/packages/app/e2e-tests/custom-theme.spec.ts[64-95]

### Suggested fix
1. Remove the global `.disableRules([...])` from the builder.
2. Instead, analyze normally and then filter out only the known-upstream violations before asserting, e.g.:
  - drop `aria-valid-attr-value` violations only for nodes where the failure is due to `aria-controls` containing `:`
  - drop `nested-interactive` violations only within the material-table demo container/selector
3. Alternatively, add an `options.disabledRules?: string[]` parameter and only pass these disables in the specific scans/pages that actually contain the upstream patterns (e.g. the tab/page that renders react-aria tabs or material-table).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Duplicate BUI token override 🐞 Bug ⚙ Maintainability
Description
--bui-fg-secondary is overridden in multiple places (static buiTokenBridge.css in apps and a dynamic
injected <style> in the theme Provider), creating competing sources of truth. This makes future
palette/token changes fragile because the effective value depends on CSS injection order and can
drift between the static and dynamic implementations.
Code

workspaces/theme/plugins/theme/src/components/ThemeProvider.tsx[R46-66]

+}) => {
+  const mui5Theme = theme.getTheme('v5') as Mui5Theme;
+  const secondary = mui5Theme.palette.text.secondary;
+  return (
+    <UnifiedThemeProvider theme={theme}>
+      <StyledEngineProvider injectFirst>
+        <Mui5Provider theme={mui5Theme}>
+          {/*
+            Native style tag (not Emotion) so the declaration is unlayered and
+            reliably overrides @backstage/ui @layer tokens for --bui-fg-secondary.
+          */}
+          <style>{`
+            :root, [data-theme-mode='light'], [data-theme-mode='dark'] {
+              --bui-fg-secondary: ${secondary} !important;
+            }
+          `}</style>
+          {children}
+        </Mui5Provider>
+      </StyledEngineProvider>
+    </UnifiedThemeProvider>
+  );
Relevance

⭐⭐ Medium

Dup token override is subjective; team sometimes rejects !important/override fragility but may
accept for a11y fix.

PR-#3513
PR-#2840

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The ThemeProvider now injects an override for --bui-fg-secondary, while the demo apps also import
a CSS file that sets the same variable with !important, creating multiple competing definitions.

workspaces/theme/plugins/theme/src/components/ThemeProvider.tsx[40-67]
workspaces/theme/packages/app/src/index.tsx[17-24]
workspaces/theme/packages/app/src/buiTokenBridge.css[17-29]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
The PR introduces multiple mechanisms that set `--bui-fg-secondary` (`buiTokenBridge.css` in the demo apps and an injected `<style>` in the shared ThemeProvider). This duplication increases the chance of precedence bugs and drift.

### Issue Context
- Apps now import a static stylesheet that sets fixed values per mode.
- ThemeProvider now injects a dynamic override based on `mui5Theme.palette.text.secondary`.

### Fix Focus Areas
- workspaces/theme/plugins/theme/src/components/ThemeProvider.tsx[40-67]
- workspaces/theme/packages/app/src/index.tsx[17-24]
- workspaces/theme/packages/app-legacy/src/index.tsx[17-24]
- workspaces/theme/packages/app/src/buiTokenBridge.css[17-29]
- workspaces/theme/packages/app-legacy/src/buiTokenBridge.css[17-29]

### Suggested fix (pick one owner)
Option A (recommended): Make ThemeProvider the single source of truth
1. Keep the ThemeProvider bridge.
2. Remove `buiTokenBridge.css` imports from both `packages/app` and `packages/app-legacy` and delete the duplicate CSS files.

Option B: Make apps own the override
1. Remove the injected `<style>` from ThemeProvider.
2. Ship a single shared CSS asset from the theme plugin and document/import it once (avoid duplicating per-app).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Signed-off-by: Yi Cai <yicai@redhat.com>
@sonarqubecloud

Copy link
Copy Markdown

@HusneShabbir HusneShabbir left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

/lgtm
the theme-owned violations look fixed, and re-enabling the axe assertions with CI green is solid.

One follow-up: the remaining scoped disables (aria-valid-attr-value from react-aria/useId() colon IDs, and nested-interactive from @material-table/core) would be good candidates for upstream issues (or PRs) so we can drop those disables later.

Other than that, looks good to me.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants