feat(tokens): repoint interactive primary color to black/white CTA - #1957
Draft
cmwinters wants to merge 4 commits into
Draft
feat(tokens): repoint interactive primary color to black/white CTA#1957cmwinters wants to merge 4 commits into
cmwinters wants to merge 4 commits into
Conversation
Points bg.interactive.primary.* at the black/white primitive ramps so the primary CTA follows the updated brand: black in light mode, white in dark. These ramps are alpha steps over #07080C / #FFFFFF, so the .900 members supply the hover and focus states by compositing over whatever sits behind the control. That lightens on hover in light mode and dims in dark mode automatically, with no per-theme CSS and no new tokens. text.interactive.primary and fill.interactive.primary are deliberately left alone. Both are white in *both* themes today, and Button's destructive variant plus destructive Menu items depend on that for their white-on-red labels -- flipping them would drop that label to 2.94:1 in dark mode, an AA failure. shadow.interactive.focus (the blue focus ring) is also unchanged.
Every foreground drawn on bg.interactive.primary was hard-locked to white in both themes, so the now-white dark-mode fill made each of them invisible. They move to text-ui-primary-inverted, which already resolves to #FFFFFF in light and #181A1F in dark: - Button .primary label and icon fill. Its hover/pressed/focus rules also drop their redundant color declarations, since the label no longer varies by state. - Checkbox checkmark and indeterminate dash. - Radio dot, which otherwise left a selected radio with no affordance. - DateField focused segment. caret-color is transparent there, so a matching label would have rendered the segment completely blank. This rule is also used by DatePicker, DateRangePicker and TimeField. - Switch primary handle and label. The handle also gets an explicit neutral shadow: the selected rule tints it green for the default variant, and the primary block previously overrode that with a hardcoded blue glow that would clash with a black or white track. Also adds .primary[data-selected], because the shared selected rule swaps the fill but not the label, leaving a selected primary toggle at roughly 1.01:1 in dark mode. The :where(button, label) guard mirrors the shared rule so this does not reach Calendar's selected date cell, which React Aria renders as a div and which should keep the inverted label on the CTA fill. The Slider thumb straddles the filled and unfilled track, so it cannot invert with the theme; it keeps its white fill and gains a 2px border for definition against the white dark-mode fill.
This package's --Button-color-text-primary and --Button-icon-color-fill-primary-* are shared by both its primary and destructive variants, so it cannot adopt an inverting label without splitting them across 11 call sites. Rather than restyle a deprecated package, its primary background is pinned to the blue it renders today, leaving it visually unchanged. The custom properties are declared on .Button itself, so an ancestor [data-theme] rule cannot override them. The dark-mode half of the pin therefore uses a selector that matches the element too.
🦋 Changeset detectedLatest commit: 921f646 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
black.900 was too subtle against an opaque black base -- the hover was barely perceptible in light mode. Moving hover and focus to black.800 roughly doubles the step, from dL* +10.5 to +21.9, and the white label still clears AAA at 11.50:1. Dark mode keeps white.900, which already read well.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements DSYS-263 (epic DSYS-158, POC Rebrand x LaunchPad).
Summary
Moves the primary CTA from LaunchDarkly blue to black in light mode / white in dark mode by repointing
bg.interactive.primary.*at the black/white primitive ramps, rather than adding a new token role.The ramps are alpha ramps —
black.0–900are alpha steps over#07080C,white.*over#FFFFFF. So the upper members supply hover and focus by compositing over whatever sits behind the control: light mode lightens on hover, dark mode dims, with nolight-dark()and no per-theme CSS..950)#07080Cblack.800→#39393D#FFFFFFwhite.900→#E8E8E9The two themes use different alpha steps on purpose.
black.900was near-imperceptible against an opaque black base, so light mode uses.800; dimming a white surface reads much more strongly, so dark mode stays at.900. The numeric deltas are asymmetric to land perceptually symmetric.Why the paired text tokens were left alone
text.interactive.primaryandfill.interactive.primaryare white in both themes today (light value, no dark override)..destructiveand destructive Menu items borrow them for their white-on-red labels, andtext.interactive.destructivecan't substitute — it resolves to{color.text.feedback.error}, i.e. red text for links and menu labels.Flipping them would have dropped the destructive label to 2.94:1 in dark mode, an AA failure. So they're untouched, destructive needs no changes at all, and the foregrounds that sit on the primary fill move to the existing
text-ui-primary-invertedinstead.Trade-off accepted:
text.interactive.primaryno longer pairs withbg.interactive.primary. Recorded in a code comment; renaming it to something liketext.interactive.on-accentis a follow-up.Foregrounds that had to be fixed
Each was hard-locked to white in both themes, so the new white dark-mode fill made it invisible:
caret-coloris transparent there, so it would have rendered completely blank. Also covers DatePicker, DateRangePicker, TimeField.rgb(64 91 255 …)) that would clash with a black or white track.Also fixed
.button[data-selected]swaps the fill but not the label, leaving a selected primary toggle at ~1.01:1 in dark mode. Added.primary[data-selected](6.69:1 light / 12.72:1 dark). The:where(button, label)guard mirrors the shared rule so it doesn't reach Calendar's selected date cell, which React Aria renders as adiv.Disabled primary also improves incidentally —
opacity: 0.64with no color token means light mode goes from a composited#8698FFat 2.64:1 (fails AA today) to roughly 6.2:1.Deliberately unchanged
shadow.interactive.focus(the blue focus ring),shadow.interactive.primary(already flips, so the inset halo is correct on both new fills),border.interactive.primary.*(transparent), and all destructive styling.Screenshots (if appropriate)
Two things worth a designer's eye specifically:
Calendar.tsxstyles them withvariant: 'primary'). Intentional, but it's the one change outside Button and the selection controls.Testing approaches
pnpm test— 273 tests / 88 files pass; token snapshot regenerated (exactly 8 changed lines, 4 keys × 2 themes)pnpm typecheckandpnpm checkcleandist/style.css:.primarypairs the CTA fill with the inverted label, the:where(button, label)guard survives compilation, the primary override follows the shared[data-selected]rule so it wins the specificity tie, and the blue glow is gone (0 occurrences of405bff)Note that unit tests cannot catch any of this —
Button.spec.tsxhas no style assertions, and the repo has no automated contrast test. Chromatic and the axe panel are the real gates, and Chromatic renders every story in both themes so each diff is doubled.Flagged for reviewer decision
The deprecated
@launchpad-ui/buttonis pinned to the previous blue rather than tracking the rebrand. Its--Button-color-text-primaryand--Button-icon-color-fill-primary-*are shared by both its primary and destructive variants, so adopting an inverting label would mean splitting them across 11 call sites. Pinning keeps it visually unchanged — but it does mean a legacy primary button stays blue while the new one goes black, so an app using both will see a mismatch. Happy to do the split instead if the team would rather it track the rebrand.