Skip to content

fix(theme): contained buttons honor color=error/success/warning#1674

Merged
leecalcote merged 3 commits into
masterfrom
fix/mui-button-honor-semantic-color
Jul 1, 2026
Merged

fix(theme): contained buttons honor color=error/success/warning#1674
leecalcote merged 3 commits into
masterfrom
fix/mui-button-honor-semantic-color

Conversation

@leecalcote

Copy link
Copy Markdown
Member

Symptom

<Button variant="contained" color="error"> (and success/warning) renders the brand (keppel) green, not the semantic colour - so every destructive action in a consuming app is silently miscoloured, and callers work around it with per-site sx specificity hacks.

Root cause

MuiButton.styleOverrides.root sets &.MuiButton-contained { backgroundColor: palette.background.brand.default } for every contained button and defines no containedError/Success/Warning. MUI's default semantic-contained styling is therefore overridden and color is ignored for the background; a consumer's flat sx.backgroundColor (specificity 0,0,1,0) also loses to the theme selector (0,0,2,0).

Fix

Add &.MuiButton-containedError / containedSuccess / containedWarning rules using the matching palette.background.{error,success,warning} tokens, layered after the brand &.MuiButton-contained rule so they win on source order at equal (0,0,2,0) specificity. The higher-specificity .Mui-disabled rules still win when disabled. primary/default and secondary are unchanged, so only the currently-broken semantic-contained buttons change.

Blast radius / review ask

Low: only color="error|success|warning" contained buttons change (they were already visually wrong). Please eyeball any surface that used variant="contained" color="success|warning" expecting the brand look. outlined variants are untouched here.

Test

src/__testing__/theme.test.ts asserts the resolved containedError/Success/Warning backgrounds are the semantic tokens (light + dark) and not the brand background. Full suite: 306 tests pass.

Downstream

Unblocks removing a per-site sx specificity workaround in layer5io/meshery-cloud (Edit-Org destructive buttons) once released and consumed.

The MuiButton styleOverride painted every .MuiButton-contained with the brand
(keppel) background regardless of the `color` prop and defined no containedError,
so `<Button variant="contained" color="error">` rendered brand green. Every
destructive action across consuming apps was silently miscoloured, and callers
had to fight it with per-site sx specificity hacks.

Add &.MuiButton-containedError / containedSuccess / containedWarning rules using
the matching palette.background.{error,success,warning} tokens, layered after the
brand &.MuiButton-contained rule so they win on source order at equal
specificity; the higher-specificity .Mui-disabled rules still win when disabled.
primary/default and secondary are unchanged, so only the currently-broken
semantic-contained buttons change.

Signed-off-by: Lee Calcote <lee.calcote@layer5.io>
Copilot AI review requested due to automatic review settings July 1, 2026 15:57

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request adds support for semantic color properties (error, success, and warning) on contained MuiButton components to prevent them from defaulting to the brand color, and includes corresponding unit tests. The reviewer's feedback highlights an accessibility issue where hardcoding white text on a warning background violates WCAG contrast guidelines, and suggests using theme.palette.getContrastText to dynamically resolve text colors. Additionally, the reviewer recommends adding support and tests for the missing info semantic color.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +34 to +54
'&.MuiButton-containedError': {
color: constant?.white,
backgroundColor: error?.default,
'&:hover': {
backgroundColor: error?.hover
}
},
'&.MuiButton-containedSuccess': {
color: constant?.white,
backgroundColor: success?.default,
'&:hover': {
backgroundColor: success?.hover
}
},
'&.MuiButton-containedWarning': {
color: constant?.white,
backgroundColor: warning?.default,
'&:hover': {
backgroundColor: warning?.hover
}
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

This block has two main issues:

  1. Accessibility / Contrast Issue: Hardcoding color: constant?.white (near-white) for the warning button results in white text on a yellow background (Colors.yellow[30] / Colors.yellow[40]), which severely violates WCAG contrast guidelines and makes the text unreadable.
  2. Missing info Button: The info semantic color is also supported by MUI buttons but was omitted, meaning <Button variant="contained" color="info"> would still incorrectly render with the brand green background.

To solve both issues robustly, we can use MUI's built-in theme.palette.getContrastText to dynamically calculate the correct WCAG-compliant text color for all semantic buttons (error, success, warning, info), and add the missing &.MuiButton-containedInfo override.

        '&.MuiButton-containedError': {
          color: error?.default ? theme.palette.getContrastText(error.default) : constant?.white,
          backgroundColor: error?.default,
          '&:hover': {
            backgroundColor: error?.hover
          }
        },
        '&.MuiButton-containedSuccess': {
          color: success?.default ? theme.palette.getContrastText(success.default) : constant?.white,
          backgroundColor: success?.default,
          '&:hover': {
            backgroundColor: success?.hover
          }
        },
        '&.MuiButton-containedWarning': {
          color: warning?.default ? theme.palette.getContrastText(warning.default) : constant?.white,
          backgroundColor: warning?.default,
          '&:hover': {
            backgroundColor: warning?.hover
          }
        },
        '&.MuiButton-containedInfo': {
          color: info?.default ? theme.palette.getContrastText(info.default) : constant?.white,
          backgroundColor: info?.default,
          '&:hover': {
            backgroundColor: info?.hover
          }
        },

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Great catch on both. Adopted theme.palette.getContrastText(...) for the label colour on all four semantic variants (so the yellow warning gets readable dark text instead of white), and added &.MuiButton-containedInfo. Done in d2e8e8c.

Comment thread src/theme/components/button.modifier.ts Outdated
const {
palette: {
background: { brand, hover },
background: { brand, hover, error, success, warning },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Destructure the info semantic color from background so it can be used to style the info contained button.

Suggested change
background: { brand, hover, error, success, warning },
background: { brand, hover, error, success, warning, info },

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done - info is now destructured from background. d2e8e8c.

Comment thread src/__testing__/theme.test.ts Outdated
Comment on lines +115 to +123
it('also honors success and warning contained buttons', () => {
const { theme, styles } = rootStyles('light');
expect(styles['&.MuiButton-containedSuccess'].backgroundColor).toBe(
theme.palette.background.success.default
);
expect(styles['&.MuiButton-containedWarning'].backgroundColor).toBe(
theme.palette.background.warning.default
);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

Add an assertion for &.MuiButton-containedInfo to verify that the info semantic color is also correctly resolved.

  it('also honors success, warning, and info contained buttons', () => {
    const { theme, styles } = rootStyles('light');
    expect(styles['&.MuiButton-containedSuccess'].backgroundColor).toBe(
      theme.palette.background.success.default
    );
    expect(styles['&.MuiButton-containedWarning'].backgroundColor).toBe(
      theme.palette.background.warning.default
    );
    expect(styles['&.MuiButton-containedInfo'].backgroundColor).toBe(
      theme.palette.background.info.default
    );
  });

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Done - the test now asserts the info background, plus a new assertion that the label colour equals the getContrastText-derived value (so the WCAG fix is covered). d2e8e8c.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes Sistent’s MuiButton theme override so contained buttons with semantic color="error" | "success" | "warning" use the corresponding semantic background tokens instead of always using the brand (keppel) background. This aligns contained button visuals with MUI’s semantic color contract and removes the need for downstream sx specificity workarounds.

Changes:

  • Extend MuiButton.styleOverrides.root to add &.MuiButton-containedError|Success|Warning background/hover rules after the brand contained rule.
  • Add Jest coverage asserting the resolved contained semantic background tokens (light + dark).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
src/theme/components/button.modifier.ts Adds contained semantic color selectors to restore correct error/success/warning backgrounds.
src/testing/theme.test.ts Adds regression tests validating semantic contained backgrounds resolve to semantic tokens (light/dark).

Comment on lines +48 to +54
'&.MuiButton-containedWarning': {
color: constant?.white,
backgroundColor: warning?.default,
'&:hover': {
backgroundColor: warning?.hover
}
},

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed - warning (and the other semantic variants) now derive the label via theme.palette.getContrastText(background), so the yellow warning stays readable in both light and dark modes. d2e8e8c.

Comment on lines +34 to +40
'&.MuiButton-containedError': {
color: constant?.white,
backgroundColor: error?.default,
'&:hover': {
backgroundColor: error?.hover
}
},

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed - the error label colour now comes from theme.palette.getContrastText(error.default) too, so it stays readable across palette modes. d2e8e8c.

Comment on lines +92 to +96
const root = MuiButton.styleOverrides!.root as (arg: { theme: typeof theme }) => Record<
string,
{ backgroundColor?: string; ['&:hover']?: { backgroundColor?: string } }
>;
return { theme, styles: root({ theme }) };

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The cast is deliberately scoped to the selector shape the test asserts (color / backgroundColor / &:hover); the test only ever indexes selector keys, never fontWeight / lineHeight. Broadening to Record<string, unknown> would add a cast at every access without catching a real bug, so I've kept the narrower shape. Happy to switch if you'd prefer the stricter typing.

Review follow-up:
- Derive the label colour with theme.palette.getContrastText per background so a
  light background (notably the yellow warning) keeps a readable dark label
  instead of an unreadable hardcoded white one (WCAG contrast).
- Add &.MuiButton-containedInfo so color="info" contained buttons also honour the
  semantic colour instead of the brand green.
- Extend the theme test to cover the info background and the contrast-derived
  label colour.

Signed-off-by: Lee Calcote <lee.calcote@layer5.io>
@leecalcote

Copy link
Copy Markdown
Member Author

Thanks for the thorough review - all addressed in d2e8e8c:

  • Accessibility (WCAG): the label colour is now derived per-background via theme.palette.getContrastText(...) on every semantic variant, so the light yellow warning gets a readable dark label instead of an unreadable white one.
  • Missing info: added &.MuiButton-containedInfo (and destructured info from background).
  • Tests: added the info background assertion plus a contrast-derived label-colour assertion.

Full suite: 307 tests pass.

Replace em-dashes / ellipses with plain ASCII in the comments touched by the
semantic-colour change (no non-ASCII punctuation in code).

Signed-off-by: Lee Calcote <lee.calcote@layer5.io>
@leecalcote leecalcote merged commit dfe8a4f into master Jul 1, 2026
5 checks passed
@leecalcote leecalcote deleted the fix/mui-button-honor-semantic-color branch July 1, 2026 17:57
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.

2 participants