diff --git a/workspaces/theme/.changeset/amber-scroll-mend.md b/workspaces/theme/.changeset/amber-scroll-mend.md
new file mode 100644
index 00000000000..e001e7e5cc4
--- /dev/null
+++ b/workspaces/theme/.changeset/amber-scroll-mend.md
@@ -0,0 +1,9 @@
+---
+'@red-hat-developer-hub/backstage-plugin-theme': patch
+'@red-hat-developer-hub/backstage-plugin-bcc-test': patch
+'@red-hat-developer-hub/backstage-plugin-bui-test': patch
+'@red-hat-developer-hub/backstage-plugin-mui4-test': patch
+'@red-hat-developer-hub/backstage-plugin-mui5-test': patch
+---
+
+Fix light-theme WCAG contrast and demo a11y issues so the theme e2e axe suite can enforce violations.
diff --git a/workspaces/theme/packages/app-legacy/e2e-tests/custom-theme.spec.ts b/workspaces/theme/packages/app-legacy/e2e-tests/custom-theme.spec.ts
index fa5ac6b3e8f..6a191b157e5 100644
--- a/workspaces/theme/packages/app-legacy/e2e-tests/custom-theme.spec.ts
+++ b/workspaces/theme/packages/app-legacy/e2e-tests/custom-theme.spec.ts
@@ -42,7 +42,6 @@ test.describe('CustomTheme should be applied', () => {
page,
testInfo,
'accessibility-scan-results.json',
- { skipViolationsAssert: true },
);
for (const theme of themes) {
@@ -86,7 +85,6 @@ test.describe('CustomTheme should be applied', () => {
page,
testInfo,
`${themeName}-${tab}-accessibility`,
- { skipViolationsAssert: true },
);
for (const subTab of subTabs) {
await page.getByRole('tab', { name: subTab }).click();
@@ -94,7 +92,6 @@ test.describe('CustomTheme should be applied', () => {
page,
testInfo,
`${themeName}-${tab}-${subTab}-accessibility`,
- { skipViolationsAssert: true },
);
}
}
diff --git a/workspaces/theme/packages/app-legacy/e2e-tests/utils/acessibility.ts b/workspaces/theme/packages/app-legacy/e2e-tests/utils/acessibility.ts
index 4854d7f9e39..30ec68a9450 100644
--- a/workspaces/theme/packages/app-legacy/e2e-tests/utils/acessibility.ts
+++ b/workspaces/theme/packages/app-legacy/e2e-tests/utils/acessibility.ts
@@ -16,6 +16,18 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, Page, TestInfo } from '@playwright/test';
+/**
+ * Upstream library false positives that cannot be fixed in the theme plugin:
+ * - aria-valid-attr-value: @backstage/ui / react-aria use React useId() IDs
+ * containing ":" in aria-controls (axe rejects colon IDREFs).
+ * - nested-interactive: @material-table/core nests focusable controls inside
+ * MuiTableSortLabel (role=button). Demo also sets options.draggable=false.
+ */
+const UPSTREAM_AXE_DISABLE_RULES = [
+ 'aria-valid-attr-value',
+ 'nested-interactive',
+] as const;
+
export async function runAccessibilityTests(
page: Page,
testInfo: TestInfo,
@@ -26,6 +38,7 @@ export async function runAccessibilityTests(
) {
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
+ .disableRules([...UPSTREAM_AXE_DISABLE_RULES])
.analyze();
await testInfo.attach(attachName, {
diff --git a/workspaces/theme/packages/app-legacy/src/buiTokenBridge.css b/workspaces/theme/packages/app-legacy/src/buiTokenBridge.css
new file mode 100644
index 00000000000..5e6df2de37f
--- /dev/null
+++ b/workspaces/theme/packages/app-legacy/src/buiTokenBridge.css
@@ -0,0 +1,29 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Override Backstage UI --bui-fg-secondary after @backstage/ui/css/styles.css.
+ * Default oklch(0% 0 0 / 50%) (~#797979) fails WCAG AA on RHDH #f2f2f2 chrome.
+ * Unlayered + !important beats BUI @layer tokens.
+ */
+:root,
+[data-theme-mode='light'] {
+ --bui-fg-secondary: #6a6e73 !important;
+}
+
+[data-theme-mode='dark'] {
+ --bui-fg-secondary: #c7c7c7 !important;
+}
diff --git a/workspaces/theme/packages/app-legacy/src/index.tsx b/workspaces/theme/packages/app-legacy/src/index.tsx
index 631b76a44dc..54acf1f2b9d 100644
--- a/workspaces/theme/packages/app-legacy/src/index.tsx
+++ b/workspaces/theme/packages/app-legacy/src/index.tsx
@@ -18,5 +18,7 @@ import '@backstage/cli/asset-types';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@backstage/ui/css/styles.css';
+// Must load after @backstage/ui styles so RHDH AA secondary text wins.
+import './buiTokenBridge.css';
ReactDOM.createRoot(document.getElementById('root')!).render();
diff --git a/workspaces/theme/packages/app/e2e-tests/custom-theme.spec.ts b/workspaces/theme/packages/app/e2e-tests/custom-theme.spec.ts
index 12a45335fe2..8a08e1935a7 100644
--- a/workspaces/theme/packages/app/e2e-tests/custom-theme.spec.ts
+++ b/workspaces/theme/packages/app/e2e-tests/custom-theme.spec.ts
@@ -42,7 +42,6 @@ test.describe('CustomTheme should be applied', () => {
page,
testInfo,
'accessibility-scan-results.json',
- { skipViolationsAssert: true },
);
for (const theme of themes) {
@@ -82,7 +81,6 @@ test.describe('CustomTheme should be applied', () => {
page,
testInfo,
`${themeName}-${tab}-accessibility`,
- { skipViolationsAssert: true },
);
for (const subTab of subTabs) {
await page.getByRole('tab', { name: subTab }).click();
@@ -90,7 +88,6 @@ test.describe('CustomTheme should be applied', () => {
page,
testInfo,
`${themeName}-${tab}-${subTab}-accessibility`,
- { skipViolationsAssert: true },
);
}
}
diff --git a/workspaces/theme/packages/app/e2e-tests/utils/acessibility.ts b/workspaces/theme/packages/app/e2e-tests/utils/acessibility.ts
index 4854d7f9e39..30ec68a9450 100644
--- a/workspaces/theme/packages/app/e2e-tests/utils/acessibility.ts
+++ b/workspaces/theme/packages/app/e2e-tests/utils/acessibility.ts
@@ -16,6 +16,18 @@
import AxeBuilder from '@axe-core/playwright';
import { expect, Page, TestInfo } from '@playwright/test';
+/**
+ * Upstream library false positives that cannot be fixed in the theme plugin:
+ * - aria-valid-attr-value: @backstage/ui / react-aria use React useId() IDs
+ * containing ":" in aria-controls (axe rejects colon IDREFs).
+ * - nested-interactive: @material-table/core nests focusable controls inside
+ * MuiTableSortLabel (role=button). Demo also sets options.draggable=false.
+ */
+const UPSTREAM_AXE_DISABLE_RULES = [
+ 'aria-valid-attr-value',
+ 'nested-interactive',
+] as const;
+
export async function runAccessibilityTests(
page: Page,
testInfo: TestInfo,
@@ -26,6 +38,7 @@ export async function runAccessibilityTests(
) {
const accessibilityScanResults = await new AxeBuilder({ page })
.withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
+ .disableRules([...UPSTREAM_AXE_DISABLE_RULES])
.analyze();
await testInfo.attach(attachName, {
diff --git a/workspaces/theme/packages/app/src/buiTokenBridge.css b/workspaces/theme/packages/app/src/buiTokenBridge.css
new file mode 100644
index 00000000000..5e6df2de37f
--- /dev/null
+++ b/workspaces/theme/packages/app/src/buiTokenBridge.css
@@ -0,0 +1,29 @@
+/*
+ * Copyright Red Hat, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Override Backstage UI --bui-fg-secondary after @backstage/ui/css/styles.css.
+ * Default oklch(0% 0 0 / 50%) (~#797979) fails WCAG AA on RHDH #f2f2f2 chrome.
+ * Unlayered + !important beats BUI @layer tokens.
+ */
+:root,
+[data-theme-mode='light'] {
+ --bui-fg-secondary: #6a6e73 !important;
+}
+
+[data-theme-mode='dark'] {
+ --bui-fg-secondary: #c7c7c7 !important;
+}
diff --git a/workspaces/theme/packages/app/src/index.tsx b/workspaces/theme/packages/app/src/index.tsx
index 6ec6cfa543a..b1eca2ae54b 100644
--- a/workspaces/theme/packages/app/src/index.tsx
+++ b/workspaces/theme/packages/app/src/index.tsx
@@ -18,5 +18,7 @@ import '@backstage/cli/asset-types';
import ReactDOM from 'react-dom/client';
import App from './App';
import '@backstage/ui/css/styles.css';
+// Must load after @backstage/ui styles so RHDH AA secondary text wins.
+import './buiTokenBridge.css';
ReactDOM.createRoot(document.getElementById('root')!).render(App.createRoot());
diff --git a/workspaces/theme/plugins/bcc-test/src/components/BCCTestPage.tsx b/workspaces/theme/plugins/bcc-test/src/components/BCCTestPage.tsx
index 8613a9c2db1..71154fef8c8 100644
--- a/workspaces/theme/plugins/bcc-test/src/components/BCCTestPage.tsx
+++ b/workspaces/theme/plugins/bcc-test/src/components/BCCTestPage.tsx
@@ -24,7 +24,9 @@ export const BCCTestPage = () => {
return (
[] = [
];
export const TableExample = () => {
- return ;
+ return (
+
+ );
};
diff --git a/workspaces/theme/plugins/bui-test/src/components/BUITestPage.tsx b/workspaces/theme/plugins/bui-test/src/components/BUITestPage.tsx
index b4f3e4ab35d..854e4ae1c53 100644
--- a/workspaces/theme/plugins/bui-test/src/components/BUITestPage.tsx
+++ b/workspaces/theme/plugins/bui-test/src/components/BUITestPage.tsx
@@ -31,7 +31,11 @@ export const BUITestPage = () => {
<>
}
+ customActions={
+
+ }
tabs={[
{
id: 'form-components',
diff --git a/workspaces/theme/plugins/bui-test/src/components/FormComponents.tsx b/workspaces/theme/plugins/bui-test/src/components/FormComponents.tsx
index ba44ee0a5f0..a8b4656e21d 100644
--- a/workspaces/theme/plugins/bui-test/src/components/FormComponents.tsx
+++ b/workspaces/theme/plugins/bui-test/src/components/FormComponents.tsx
@@ -100,8 +100,10 @@ const Checkboxes = () => {
Checkboxes
-
-
+
+
+ {' '}
+
Checkbox
Checkbox
@@ -114,8 +116,8 @@ const Switches = () => {
Switches
-
-
+
+
diff --git a/workspaces/theme/plugins/mui4-test/src/components/InlineStyles.tsx b/workspaces/theme/plugins/mui4-test/src/components/InlineStyles.tsx
index 7c80fca8dbf..ef6ef2be613 100644
--- a/workspaces/theme/plugins/mui4-test/src/components/InlineStyles.tsx
+++ b/workspaces/theme/plugins/mui4-test/src/components/InlineStyles.tsx
@@ -21,7 +21,9 @@ import { makeStyles } from '@material-ui/core/styles';
const useStyles = makeStyles(theme => ({
withPadding: {
padding: 32,
- backgroundColor: 'gray',
+ // CSS 'gray' (#808080) fails WCAG AA with white label text (3.94:1).
+ backgroundColor: '#424242',
+ color: '#ffffff',
},
withSpacing: {
padding: theme.spacing(4),
@@ -40,7 +42,7 @@ export const InlineStyles = () => {
Default button with{' '}
{
- "makeStyles({ withPadding: { padding: 32, backgroundColor: 'gray' }})"
+ "makeStyles({ withPadding: { padding: 32, backgroundColor: '#424242', color: '#ffffff' }})"
}
@@ -58,9 +60,17 @@ export const InlineStyles = () => {
Default button with{' '}
- {"style={{ padding: 32, backgroundColor: 'gray' }}"}
+
+ {
+ "style={{ padding: 32, backgroundColor: '#424242', color: '#ffffff' }}"
+ }
+
-
+
);
};
diff --git a/workspaces/theme/plugins/mui4-test/src/components/MUI4TestPage.tsx b/workspaces/theme/plugins/mui4-test/src/components/MUI4TestPage.tsx
index af727ab561d..db7e04123af 100644
--- a/workspaces/theme/plugins/mui4-test/src/components/MUI4TestPage.tsx
+++ b/workspaces/theme/plugins/mui4-test/src/components/MUI4TestPage.tsx
@@ -27,7 +27,9 @@ export const MUI4TestPage = () => {
return (
diff --git a/workspaces/theme/plugins/mui5-test/src/components/InlineStyles.tsx b/workspaces/theme/plugins/mui5-test/src/components/InlineStyles.tsx
index 3b4c648e9d0..a1366c6bb1a 100644
--- a/workspaces/theme/plugins/mui5-test/src/components/InlineStyles.tsx
+++ b/workspaces/theme/plugins/mui5-test/src/components/InlineStyles.tsx
@@ -25,9 +25,17 @@ export const InlineStyles = () => {
Default button with{' '}
- {"style={{ padding: 32, backgroundColor: 'gray' }}"}
+
+ {
+ "style={{ padding: 32, backgroundColor: '#424242', color: '#ffffff' }}"
+ }
+
-
+
Default button with{' '}
diff --git a/workspaces/theme/plugins/mui5-test/src/components/MUI5TestPage.tsx b/workspaces/theme/plugins/mui5-test/src/components/MUI5TestPage.tsx
index f67e574be3e..aaf5c1f5d1a 100644
--- a/workspaces/theme/plugins/mui5-test/src/components/MUI5TestPage.tsx
+++ b/workspaces/theme/plugins/mui5-test/src/components/MUI5TestPage.tsx
@@ -27,7 +27,9 @@ export const MUI5TestPage = () => {
return (
diff --git a/workspaces/theme/plugins/theme/dev/ThemeTestPage.tsx b/workspaces/theme/plugins/theme/dev/ThemeTestPage.tsx
index 472c520bdee..9aa4cc29de8 100644
--- a/workspaces/theme/plugins/theme/dev/ThemeTestPage.tsx
+++ b/workspaces/theme/plugins/theme/dev/ThemeTestPage.tsx
@@ -21,7 +21,9 @@ import { UserSettingsThemeToggle } from '@backstage/plugin-user-settings';
export const ThemeTestPage = () => (
diff --git a/workspaces/theme/plugins/theme/src/components/ThemeProvider.tsx b/workspaces/theme/plugins/theme/src/components/ThemeProvider.tsx
index db96223389c..1032dc90f68 100644
--- a/workspaces/theme/plugins/theme/src/components/ThemeProvider.tsx
+++ b/workspaces/theme/plugins/theme/src/components/ThemeProvider.tsx
@@ -43,15 +43,28 @@ const ThemeProvider = ({
}: {
theme: UnifiedTheme;
children: ReactNode;
-}) => (
-
-
-
- {children}
-
-
-
-);
+}) => {
+ const mui5Theme = theme.getTheme('v5') as Mui5Theme;
+ const secondary = mui5Theme.palette.text.secondary;
+ return (
+
+
+
+ {/*
+ Native style tag (not Emotion) so the declaration is unlayered and
+ reliably overrides @backstage/ui @layer tokens for --bui-fg-secondary.
+ */}
+
+ {children}
+
+
+
+ );
+};
export const createThemeProvider = (
theme: UnifiedTheme,
diff --git a/workspaces/theme/plugins/theme/src/lightTheme.ts b/workspaces/theme/plugins/theme/src/lightTheme.ts
index b68bb408d57..25730f8d1e8 100644
--- a/workspaces/theme/plugins/theme/src/lightTheme.ts
+++ b/workspaces/theme/plugins/theme/src/lightTheme.ts
@@ -40,7 +40,15 @@ export const lightThemeOverrides: Partial = {
},
text: {
primary: '#151515',
- secondary: '#757575',
+ // PatternFly --pf-v5-global--Color--200; AA on white and #f2f2f2 nav chrome
+ secondary: '#6A6E73',
+ },
+ // MUI defaults (#0288d1 / #ed6c02) fail WCAG AA on white (~3.1–3.9:1)
+ info: {
+ main: '#0066CC',
+ },
+ warning: {
+ main: '#C05600',
},
background: {
default: '#FFFFFF',