Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/cta-color-black-white.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@launchpad-ui/tokens': minor
'@launchpad-ui/components': patch
'@launchpad-ui/button': patch
---

Repoint the `bg.interactive.primary` color aliases to the brand's black/white ramps — black in light mode, white in dark mode — so primary CTAs and selection controls follow the updated brand. The blue focus ring and all destructive styling are unchanged, and the deprecated `@launchpad-ui/button` package is pinned to the previous blue. Also fixes an unreadable label on selected primary toggle buttons and improves primary button contrast in the disabled state.
28 changes: 23 additions & 5 deletions packages/button/src/styles/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,17 @@
--Button-color-text-default-active: var(--lp-color-text-ui-primary-base);

/* ------- PRIMARY ------- */
--Button-color-background-primary: var(--lp-color-bg-interactive-primary-base);
--Button-color-background-primary-hover: var(--lp-color-bg-interactive-primary-hover);
--Button-color-background-primary-focus: var(--lp-color-bg-interactive-primary-focus);
--Button-color-background-primary-active: var(--lp-color-bg-interactive-primary-active);
/* This deprecated package is pinned to the previous blue rather than tracking
bg-interactive-primary, which is now the black/white brand CTA color. Its
--Button-color-text-primary and --Button-icon-color-fill-primary-* are
shared with the destructive variant, so it cannot adopt an inverting label
without splitting them across 11 call sites. Pinning keeps the deprecated
package visually unchanged; use @launchpad-ui/components for the new CTA.
Dark-mode values are restored in the [data-theme='dark'] rule below. */
--Button-color-background-primary: var(--lp-color-blue-500);
--Button-color-background-primary-hover: var(--lp-color-blue-600);
--Button-color-background-primary-focus: var(--lp-color-blue-600);
--Button-color-background-primary-active: var(--lp-color-blue-500);
--Button-color-border-primary: var(--lp-color-border-interactive-primary-base);
--Button-color-border-primary-hover: var(--lp-color-border-interactive-primary-hover);
--Button-color-border-primary-focus: var(--lp-color-border-interactive-primary-focus);
Expand All @@ -126,7 +133,7 @@
--Button-color-text-primary-hover: var(--lp-color-text-interactive-primary-hover);

/* PRIMARY WITH ICON */
--Button-color-background-primary-withIcon: var(--lp-color-bg-interactive-primary-base);
--Button-color-background-primary-withIcon: var(--lp-color-blue-500);
--Button-color-border-primary-withIcon: var(--lp-color-border-interactive-primary-base);
--Button-color-text-primary-withIcon: var(--lp-color-text-interactive-primary-base);

Expand Down Expand Up @@ -255,6 +262,17 @@
--Button-color-text-minimal-flair-active: var(--lp-color-text-interactive-flair-active);
}

/* Dark-mode half of the primary pin described above. These custom properties are
declared on .Button itself, so an ancestor [data-theme] rule cannot override
them — the selector has to match the element too. */
[data-theme='dark'] :is(.Button, .ButtonGroup) {
--Button-color-background-primary: var(--lp-color-blue-600);
--Button-color-background-primary-hover: var(--lp-color-blue-500);
--Button-color-background-primary-focus: var(--lp-color-blue-500);
--Button-color-background-primary-active: var(--lp-color-blue-600);
--Button-color-background-primary-withIcon: var(--lp-color-blue-600);
}

.Button {
display: inline-flex;
align-items: center;
Expand Down
22 changes: 17 additions & 5 deletions packages/components/src/styles/Button.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -62,32 +62,35 @@
background-color: var(--lp-color-bg-interactive-secondary-focus);
}

/* variant="primary" is the brand CTA: black in light mode, white in dark mode.
The label uses text-ui-primary-inverted rather than text-interactive-primary
because the latter is white in BOTH themes — .destructive below depends on
that for its white-on-red label, so it must not be made to flip. */
.primary {
composes: button;
--lp-button-padding: 1px;
background-color: var(--lp-color-bg-interactive-primary-base);
color: var(--lp-color-text-interactive-primary-base);
color: var(--lp-color-text-ui-primary-inverted);
z-index: 1;

& [data-icon] {
fill: var(--lp-color-text-interactive-primary-base);
fill: var(--lp-color-text-ui-primary-inverted);
}
}

/* The state rules only shift the fill; the label color is identical across
states, so it stays declared once above. */
.primary[data-hovered] {
background-color: var(--lp-color-bg-interactive-primary-hover);
color: var(--lp-color-text-interactive-primary-hover);
}

.primary[data-pressed] {
background-color: var(--lp-color-bg-interactive-primary-active);
color: var(--lp-color-text-interactive-primary-active);
}

.primary[data-focus-visible] {
background-color: var(--lp-color-bg-interactive-primary-focus);
box-shadow: inset 0 0 0 3px var(--lp-color-shadow-interactive-primary);
color: var(--lp-color-text-interactive-primary-focus);
}

.destructive {
Expand Down Expand Up @@ -139,6 +142,15 @@
z-index: 1;
}

/* The rule above swaps the fill but not the label, so a selected primary toggle
would keep the CTA's inverted label on a pale/dark blue fill — unreadable in
both themes. The :where(button, label) guard mirrors the rule above so this
does not reach Calendar's selected date cell, which RAC renders as a div and
which should keep its inverted label on the CTA fill. */
.primary[data-selected]:where(button, label) {
color: var(--lp-color-text-interactive-selected);
}

.button[data-pending] {
cursor: wait;

Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/styles/Checkbox.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
box-shadow 0ms;

& .icon {
fill: var(--lp-color-fill-interactive-primary);
/* Sits on bg-interactive-primary when selected, so it must invert with
the theme — white on the black fill, dark on the white fill. */
fill: var(--lp-color-text-ui-primary-inverted);
width: var(--lp-size-16);
height: var(--lp-size-16);
}
Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/styles/DateField.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@

&:is([data-focus-visible], [data-focused]) {
background-color: var(--lp-color-bg-interactive-primary-base);
color: var(--lp-color-text-interactive-primary-base);
/* Must invert with the theme: caret-color is transparent below, so if the
digits matched the fill the focused segment would render blank. */
color: var(--lp-color-text-ui-primary-inverted);
caret-color: transparent;
border-radius: var(--lp-border-radius-regular);
}
Expand Down
4 changes: 3 additions & 1 deletion packages/components/src/styles/Radio.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
outline-offset 0ms;

& .icon {
fill: var(--lp-color-fill-interactive-primary);
/* Sits on bg-interactive-primary when selected, so it must invert with
the theme — white on the black fill, dark on the white fill. */
fill: var(--lp-color-text-ui-primary-inverted);
width: var(--lp-size-16);
height: var(--lp-size-16);
}
Expand Down
5 changes: 4 additions & 1 deletion packages/components/src/styles/Slider.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,10 @@
height: var(--slider-thumb-size);
border-radius: var(--lp-border-radius-large);
background-color: var(--lp-color-white-950);
border: 1px solid var(--lp-color-border-interactive-secondary-base);
/* The thumb straddles the filled and unfilled track, so it can't invert with
the theme. It stays white and relies on this border for definition against
the white fill in dark mode. */
border: var(--lp-border-width-200) solid var(--lp-color-border-interactive-secondary-base);
box-shadow:
0 0 1px 0 rgb(33 33 33 / 0.75),
0 0 2px 0 rgb(33 33 33 / 0.06),
Expand Down
18 changes: 15 additions & 3 deletions packages/components/src/styles/Switch.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -75,17 +75,29 @@
}
}

/* The primary track is the brand CTA color — black in light mode, white in dark.
Both the handle and the label sit on it, so each has to invert with the theme
rather than inherit the white treatment the default (green) variant uses. */
.switch.primary[data-selected] {
& .track {
background-color: var(--lp-color-bg-interactive-primary-base);
border: 1px solid var(--lp-color-bg-interactive-primary-base);
}

& .handle {
background-color: var(--lp-color-text-ui-primary-inverted);
/* Neutral, matching the unselected handle. The selected rule above tints
this green for the default variant, which would clash with a black or
white track. */
box-shadow:
0 0 1px 0 rgb(64 91 255 / 0.75),
0 0 2px 0 rgb(64 91 255 / 0.06),
0 0 1px 0 rgb(64 91 255 / 0.35);
0 0 1px 0 rgb(33 33 33 / 0.75),
0 0 2px 0 rgb(33 33 33 / 0.06),
0 0 1px 0 rgb(33 33 33 / 0.35);
}

& .label {
color: var(--lp-color-text-ui-primary-inverted);
text-shadow: none;
}
}

Expand Down
9 changes: 9 additions & 0 deletions packages/components/stories/ToggleButton.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,12 @@ export const Example: Story = {
await userEvent.click(canvas.getByRole('button'));
},
};

/**
* A selected `primary` toggle takes its background from the shared selected state
* rather than the CTA fill, so its label needs the selected text color to stay
* readable in both themes.
*/
export const PrimarySelected: Story = {
args: { children: 'Toggle', variant: 'primary', defaultSelected: true },
};
16 changes: 8 additions & 8 deletions packages/tokens/__tests__/__snapshots__/tokens.spec.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,10 @@ exports[`Tokens > builds dark tokens 1`] = `
--lp-color-bg-feedback-info: var(--lp-color-blue-900);
--lp-color-bg-feedback-success: var(--lp-color-green-900);
--lp-color-bg-feedback-warning: var(--lp-color-yellow-900);
--lp-color-bg-interactive-primary-base: var(--lp-color-blue-600);
--lp-color-bg-interactive-primary-active: var(--lp-color-blue-600);
--lp-color-bg-interactive-primary-focus: var(--lp-color-blue-500);
--lp-color-bg-interactive-primary-hover: var(--lp-color-blue-500);
--lp-color-bg-interactive-primary-base: var(--lp-color-white-950);
--lp-color-bg-interactive-primary-active: var(--lp-color-white-950);
--lp-color-bg-interactive-primary-focus: var(--lp-color-white-900);
--lp-color-bg-interactive-primary-hover: var(--lp-color-white-900);
--lp-color-bg-interactive-secondary-focus: var(--lp-color-gray-800);
--lp-color-bg-interactive-secondary-hover: var(--lp-color-gray-800);
--lp-color-bg-interactive-tertiary-focus: var(--lp-color-gray-800);
Expand Down Expand Up @@ -230,10 +230,10 @@ exports[`Tokens > builds default tokens 1`] = `
--lp-color-bg-feedback-info: var(--lp-color-blue-50);
--lp-color-bg-feedback-success: var(--lp-color-green-50);
--lp-color-bg-feedback-warning: var(--lp-color-yellow-50);
--lp-color-bg-interactive-primary-base: var(--lp-color-blue-500);
--lp-color-bg-interactive-primary-active: var(--lp-color-blue-500);
--lp-color-bg-interactive-primary-focus: var(--lp-color-blue-600);
--lp-color-bg-interactive-primary-hover: var(--lp-color-blue-600);
--lp-color-bg-interactive-primary-base: var(--lp-color-black-950);
--lp-color-bg-interactive-primary-active: var(--lp-color-black-950);
--lp-color-bg-interactive-primary-focus: var(--lp-color-black-800);
--lp-color-bg-interactive-primary-hover: var(--lp-color-black-800);
--lp-color-bg-interactive-secondary-focus: var(--lp-color-gray-50);
--lp-color-bg-interactive-secondary-hover: var(--lp-color-gray-50);
--lp-color-bg-interactive-tertiary-focus: var(--lp-color-gray-50);
Expand Down
8 changes: 4 additions & 4 deletions packages/tokens/tokens/color-aliases.dark.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,16 +19,16 @@
"interactive": {
"primary": {
"base": {
"$value": "{color.blue.600}"
"$value": "{color.white.950}"
},
"active": {
"$value": "{color.blue.600}"
"$value": "{color.white.950}"
},
"focus": {
"$value": "{color.blue.500}"
"$value": "{color.white.900}"
},
"hover": {
"$value": "{color.blue.500}"
"$value": "{color.white.900}"
}
},
"secondary": {
Expand Down
8 changes: 4 additions & 4 deletions packages/tokens/tokens/color-aliases.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,16 @@
"interactive": {
"primary": {
"base": {
"$value": "{color.blue.500}"
"$value": "{color.black.950}"
},
"active": {
"$value": "{color.blue.500}"
"$value": "{color.black.950}"
},
"focus": {
"$value": "{color.blue.600}"
"$value": "{color.black.800}"
},
"hover": {
"$value": "{color.blue.600}"
"$value": "{color.black.800}"
}
},
"secondary": {
Expand Down
Loading