Skip to content

[rebrand] Hold Bootstrap button brand colors on compound states (RE-177) - #74706

Merged
levadadenys merged 1 commit into
stagingfrom
denys/re/bootstrap-button-compound-states
Aug 19, 2026
Merged

[rebrand] Hold Bootstrap button brand colors on compound states (RE-177)#74706
levadadenys merged 1 commit into
stagingfrom
denys/re/bootstrap-button-compound-states

Conversation

@levadadenys

@levadadenys levadadenys commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

Before:
before-1-idle
before-2-hover
before-3-pending
before-4-page
before-5-focus
before-prod-1-idle
before-prod-3-pending-hovered

After:
after-1-idle
after-2-hover
after-3-pending-hovered
after-3-pending
after-4-page
after-5-focus
after-6-jquery-disabled

Problem

RE-177 reported the levelgroup submit button's pending state as still orange. It is, and so are three sibling states on every .btn-primary in dashboard.

Bootstrap's button-variant mixin bakes the Sass $background a variant was generated from into two nested blocks whose selectors are specificity 0,3,0:

&:active, &.active, .open > &.dropdown-toggle {
  &:hover, &:focus, &.focus { background-color: darken($background, 17%); }
}
&.disabled, &[disabled], fieldset[disabled] & {
  &:hover, &:focus, &.focus { background-color: $background; }
}

The rebrand's .btn-primary / .btn-success overrides are flat &:hover / &:active rules at 0,2,0, so they lose to both. $btn-primary-bg is still $orange.

Every one of these states needs the pointer, focus, or the disabled attribute on the button, so none of them shows at rest — which is why they survived the rebrand sweep and why they are easy to miss when checking in devtools without a hover.

Fix

One file. Mirror the mixin's own selector shape so the specificity ties and source order decides. Both blocks live in a single themable-button-states mixin taking keyword arguments, so a variant cannot be half migrated:

@include themable-button-states(
  $default: var(--background-brand-purple-primary),
  $default-border: var(--borders-brand-purple-primary),
  $strong: var(--background-brand-purple-strong, #{$dark_purple}),
  $strong-border: var(--borders-brand-purple-strong, #{$dark_purple})
);

.btn-success had the same latent bug, reverting to stock Bootstrap rgb(92,184,92) instead of our rgb(37,136,48).

Verification

Measured with CSS.forcePseudoState over CDP so every state is read rather than inferred — against the external-level Continue button and the levelgroup Submit button:

state production with fix
rest rgb(76,66,207) rgb(76,66,207)
hover rgb(43,30,159) rgb(43,30,159)
focus rgb(43,30,159) rgb(43,30,159)
pressed rgb(168,108,0) rgb(43,30,159)
pressed via keyboard rgb(168,108,0) rgb(43,30,159)
disabled rgb(76,66,207) rgb(76,66,207)
disabled + hover rgb(255,164,0) rgb(76,66,207)
disabled + focus rgb(255,164,0) rgb(76,66,207)

Cascade resolution confirmed for all 10 compound selectors across both variants.

The disabled pair is most visible on submit buttons — the pointer stays over the button for as long as the submission is in flight — and on _pixelation.html.haml's #finished button, which ships disabled from page load.

I also audited the rest of the mixin for anything else baked at compile time. Only .badge { color: $background } remains, and nothing in the repo puts a badge inside a button, so this is the complete surface.

Eyes

No baseline should move. Every changed state requires pointer, focus, or the disabled attribute; the at-rest paint is byte-identical. Flagging it because 21 eyes-tagged scenarios touch pages with these buttons.

Considered and not done here

Replacing these with design-system MUI buttons. It works, but eight buttons in five sibling partials (_external, _standalone_video, _external_link, _curriculum_reference, _locked_lesson) would stay Bootstrap, and the metrics differ — 44px/6px radius/17.5px font vs 42px/4px/16px. A student would get an MUI Submit on a levelgroup and a Bootstrap Continue one level later, so a partial migration reads as worse consistency than today. Worth its own ticket covering all 11 partials, budgeted for the eyes baselines.

🤖 Generated with Claude Code

Bootstrap's button-variant mixin bakes the Sass $background a variant was
generated from into two nested blocks whose selectors are specificity 0,3,0:

  &:active, &.active, .open > &.dropdown-toggle {
    &:hover, &:focus, &.focus { background-color: darken($background, 17%); }
  }
  &.disabled, &[disabled], fieldset[disabled] & {
    &:hover, &:focus, &.focus { background-color: $background; }
  }

The rebrand's .btn-primary and .btn-success overrides are flat `&:hover` /
`&:active` rules at 0,2,0, so they lose to both.  $btn-primary-bg is still
$orange, which left four orange states on every .btn-primary in dashboard:

                        before             after
  pressed               rgb(168,108,0)     rgb(43,30,159)
  pressed via keyboard  rgb(168,108,0)     rgb(43,30,159)
  disabled + hover      rgb(255,164,0)     rgb(76,66,207)
  disabled + focus      rgb(255,164,0)     rgb(76,66,207)

Each needs the pointer, focus, or the disabled attribute on the button, so none
of them shows at rest - which is why they survived the rebrand sweep.  The
disabled pair is most visible on submit buttons, where the pointer is still over
the button for as long as the submission is in flight, and on the pixelation
level's #finished button, which ships disabled from page load.

Mirror the mixin's own selector shape so the specificity ties and source order
decides.  Both blocks live in one mixin, taking keyword arguments, so a variant
cannot be half migrated.

.btn-success had the same latent bug, reverting to stock Bootstrap
rgb(92,184,92) instead of rgb(37,136,48).

Verified with CSS.forcePseudoState against the external-level Continue button
and the levelgroup Submit button, reading every state the mixin touches.  No
eyes baseline should move: the at-rest paint is unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@levadadenys
levadadenys force-pushed the denys/re/bootstrap-button-compound-states branch from 753c6cc to 9071254 Compare August 18, 2026 16:59
@levadadenys
levadadenys requested a lite review from Copilot August 18, 2026 17:01
@levadadenys
levadadenys requested review from a team and Nokondi and removed request for a team August 18, 2026 17:01

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

This PR fixes a Bootstrap rebrand regression where .btn-primary and .btn-success could revert to Bootstrap’s compile-time colors in compound interaction states (notably active+hover/focus and disabled+hover/focus), by matching Bootstrap’s nested selector specificity so the runtime CSS variable overrides win.

Changes:

  • Add a themable-button-states Sass mixin that mirrors Bootstrap’s nested selectors for :active/.active and disabled compound states.
  • Apply the mixin to .btn-primary and .btn-success so compound states consistently use the intended runtime brand CSS variables.
  • Fix a comment typo (“Bootstramp” → “Bootstrap”).

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@levadadenys
levadadenys merged commit 8ad64b2 into staging Aug 19, 2026
9 checks passed
@levadadenys
levadadenys deleted the denys/re/bootstrap-button-compound-states branch August 19, 2026 14:51
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.

3 participants