From 60c8af12b634aaa4a945b70321c99b018586cd65 Mon Sep 17 00:00:00 2001 From: Preston Booth Date: Wed, 28 Oct 2026 10:26:26 -0600 Subject: [PATCH 1/3] feat(mosaic): split control and container borders and use washes for states Co-Authored-By: Claude Sonnet 5.5 --- .changeset/mosaic-color-adoption.md | 5 ++ .../action-bar/action-bar.styles.ts | 2 +- .../src/components/avatar/avatar.styles.ts | 2 +- .../src/components/badge/badge.styles.ts | 11 ++-- .../src/components/banner/banner.styles.ts | 2 +- .../src/components/button/button.styles.ts | 51 +++++++++++-------- .../mosaic/src/components/card/card.styles.ts | 4 +- .../components/data-list/data-list.styles.ts | 4 +- .../src/components/drawer/drawer.styles.ts | 4 +- .../src/components/icon/icon-frame.styles.ts | 6 ++- .../mosaic/src/components/item/item.styles.ts | 2 +- .../mosaic/src/components/menu/menu.styles.ts | 2 +- .../pagination/pagination.styles.ts | 4 +- .../phone-input/phone-input.styles.ts | 4 +- .../src/components/profile/profile.styles.ts | 4 +- .../src/components/section/section.styles.ts | 6 +-- .../src/components/table/table.styles.ts | 4 +- .../components/tag-input/tag-input.styles.ts | 2 +- .../src/components/toast/toast.styles.ts | 4 +- ...-profile-billing-history-section.styles.ts | 6 +-- .../user-profile-add-authenticator.styles.ts | 2 +- .../user-profile-backup-codes.styles.ts | 4 +- packages/swingset/src/stories/button.mdx | 2 +- packages/swingset/src/stories/icon-frame.mdx | 2 +- 24 files changed, 76 insertions(+), 63 deletions(-) create mode 100644 .changeset/mosaic-color-adoption.md diff --git a/.changeset/mosaic-color-adoption.md b/.changeset/mosaic-color-adoption.md new file mode 100644 index 00000000000..8bf389981a3 --- /dev/null +++ b/.changeset/mosaic-color-adoption.md @@ -0,0 +1,5 @@ +--- +'@clerk/mosaic': patch +--- + +Controls such as inputs, selects, checkboxes, radios and outline buttons keep the stronger `--cl-color-border`, while containers and dividers (cards, sections, tables, menus, the profile nav) use `--cl-color-border-subtle`. Outline buttons fill with the surface color and lay their hover and pressed tint over it, so the tint works on the page, the surface or a faded fill. Banners, badges, toasts and the tag-input error use the `-subtle` and `-border` tokens, and the neutral badge and banner use `--cl-color-neutral-subtle`. The negative color is slightly deeper in light mode for contrast. diff --git a/packages/mosaic/src/components/action-bar/action-bar.styles.ts b/packages/mosaic/src/components/action-bar/action-bar.styles.ts index 8c9e43c612e..8f259da05b9 100644 --- a/packages/mosaic/src/components/action-bar/action-bar.styles.ts +++ b/packages/mosaic/src/components/action-bar/action-bar.styles.ts @@ -79,7 +79,7 @@ export const styles = stylex.create({ marginBlock: space['1'], marginInline: space['0.5'], alignSelf: 'stretch', - backgroundColor: colorVars['--cl-color-border'], + backgroundColor: colorVars['--cl-color-border-subtle'], width: '1px', }, }); diff --git a/packages/mosaic/src/components/avatar/avatar.styles.ts b/packages/mosaic/src/components/avatar/avatar.styles.ts index 2da226047e0..2be8ff4b4e7 100644 --- a/packages/mosaic/src/components/avatar/avatar.styles.ts +++ b/packages/mosaic/src/components/avatar/avatar.styles.ts @@ -67,7 +67,7 @@ export const styles = stylex.create({ }, icon: { - borderColor: colorVars['--cl-color-border'], + borderColor: colorVars['--cl-color-border-subtle'], borderRadius: radiusVars['--cl-radius-full'], borderStyle: 'solid', borderWidth: '1px', diff --git a/packages/mosaic/src/components/badge/badge.styles.ts b/packages/mosaic/src/components/badge/badge.styles.ts index 905cdc395cb..aacab9c9c78 100644 --- a/packages/mosaic/src/components/badge/badge.styles.ts +++ b/packages/mosaic/src/components/badge/badge.styles.ts @@ -2,12 +2,9 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, fontFamilyVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; -// warning/negative/positive fill with the subtle surface and use the saturated token as text; -// primary fills with the solid token and uses its `-foreground` for text. -// -// Neutral has no subtle surface to fill — `--cl-color-foreground-secondary` is a text gray, and its -// `-foreground` is a text color rather than an on-fill one, so it is unreadable against the solid -// 900. It rides the same neutral scrim the button's neutral fill does. +// Every color but primary fills with its `-subtle` token and uses the saturated token as text; +// primary fills with the solid token and uses its `-foreground` for text. Every `-subtle` is a +// wash, so the chip stays visible on the page, the surface and the faded surface alike. export const styles = stylex.create({ base: { borderRadius: radiusVars['--cl-radius-full'], @@ -33,7 +30,7 @@ export const colors = stylex.create({ color: colorVars['--cl-color-brand-foreground'], }, neutral: { - backgroundColor: colorVars['--cl-color-neutral-alpha-100'], + backgroundColor: colorVars['--cl-color-neutral-subtle'], color: colorVars['--cl-color-foreground'], }, warning: { diff --git a/packages/mosaic/src/components/banner/banner.styles.ts b/packages/mosaic/src/components/banner/banner.styles.ts index a248540fcf3..3fab7411dda 100644 --- a/packages/mosaic/src/components/banner/banner.styles.ts +++ b/packages/mosaic/src/components/banner/banner.styles.ts @@ -39,7 +39,7 @@ export const styles = stylex.create({ export const rootColors = stylex.create({ neutral: { borderColor: colorVars['--cl-color-border'], - backgroundColor: colorVars['--cl-color-neutral-alpha-100'], + backgroundColor: colorVars['--cl-color-neutral-subtle'], }, warning: { borderColor: colorVars['--cl-color-warning-border'], diff --git a/packages/mosaic/src/components/button/button.styles.ts b/packages/mosaic/src/components/button/button.styles.ts index 33d2bbd03db..9034e26cc8c 100644 --- a/packages/mosaic/src/components/button/button.styles.ts +++ b/packages/mosaic/src/components/button/button.styles.ts @@ -13,14 +13,20 @@ import { import { iconScope } from '../icon/icon.markers.stylex'; // Neutral fills are the three neutral alpha steps, shared across variants: -// 100 — `filled-neutral` rest, `outline-*`/`ghost-*` hover -// 200 — `filled-neutral` hover, `outline-*`/`ghost-*` pressed +// 100 — `filled-neutral` rest, `ghost-*` hover +// 200 — `filled-neutral` hover, `ghost-*` pressed // 300 — `filled-neutral` pressed // Opaque fills blend toward their own on-fill at the same 12%/18%. Neutral has no fill // to blend from, so it rides the opacity ramp above. +// Outline variants keep the surface as their fill and lay the hover and pressed wash over it as an +// inset shadow, so the state tint works on any backdrop and still transitions. const primaryHover = `color-mix(in oklab, ${colorVars['--cl-color-brand']}, ${colorVars['--cl-color-brand-foreground']} 12%)`; const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-brand']}, ${colorVars['--cl-color-brand-foreground']} 18%)`; +const neutralHoverWash = `inset 0 0 0 100vmax ${colorVars['--cl-color-neutral-alpha-100']}`; +const neutralPressedWash = `inset 0 0 0 100vmax ${colorVars['--cl-color-neutral-alpha-200']}`; +const negativeHoverWash = `inset 0 0 0 100vmax ${colorVars['--cl-color-negative-alpha-200']}`; +const negativePressedWash = `inset 0 0 0 100vmax ${colorVars['--cl-color-negative-alpha-300']}`; const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`; const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`; @@ -75,7 +81,7 @@ export const styles = stylex.create({ ':not([data-disabled]):active': durationVars['--cl-duration-instant'], ':not([data-disabled]):hover': durationVars['--cl-duration-instant'], }, - transitionProperty: 'background-color, border-color, color, opacity, text-decoration-color', + transitionProperty: 'background-color, border-color, box-shadow, color, opacity, text-decoration-color', // Linear, not `--cl-ease-default`: nothing here moves. An ease on already non-uniform // color interpolation just drags the midpoint, and the house curve's overshoot would // extrapolate past the target color. @@ -181,39 +187,42 @@ export const variants = stylex.create({ // fill, and leaves it independently themeable. 'outline-primary': { borderColor: colorVars['--cl-color-border'], - backgroundColor: { - default: 'transparent', - ':not([data-disabled]):active': colorVars['--cl-color-neutral-alpha-200'], - ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-alpha-200'], + backgroundColor: colorVars['--cl-color-surface'], + boxShadow: { + default: null, + ':not([data-disabled]):active': neutralPressedWash, + ':not([data-disabled])[data-open]': neutralPressedWash, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-100'], + ':not([data-disabled]):hover:not(:active):not([data-open])': neutralHoverWash, }, }, color: colorVars['--cl-color-brand'], }, 'outline-neutral': { borderColor: colorVars['--cl-color-border'], - backgroundColor: { - default: 'transparent', - ':not([data-disabled]):active': colorVars['--cl-color-neutral-alpha-200'], - ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-alpha-200'], + backgroundColor: colorVars['--cl-color-surface'], + boxShadow: { + default: null, + ':not([data-disabled]):active': neutralPressedWash, + ':not([data-disabled])[data-open]': neutralPressedWash, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-100'], + ':not([data-disabled]):hover:not(:active):not([data-open])': neutralHoverWash, }, }, color: colorVars['--cl-color-foreground'], }, 'outline-negative': { borderColor: colorVars['--cl-color-border'], - backgroundColor: { - default: 'transparent', - ':not([data-disabled]):active': colorVars['--cl-color-neutral-alpha-200'], - ':not([data-disabled])[data-open]': colorVars['--cl-color-neutral-alpha-200'], + backgroundColor: colorVars['--cl-color-surface'], + boxShadow: { + default: null, + ':not([data-disabled]):active': negativePressedWash, + ':not([data-disabled])[data-open]': negativePressedWash, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-neutral-alpha-100'], + ':not([data-disabled]):hover:not(:active):not([data-open])': negativeHoverWash, }, }, color: colorVars['--cl-color-negative'], @@ -248,11 +257,11 @@ export const variants = stylex.create({ 'ghost-negative': { backgroundColor: { default: 'transparent', - ':not([data-disabled]):active': `color-mix(in oklab, ${colorVars['--cl-color-negative-subtle']}, ${colorVars['--cl-color-negative']} 8%)`, - ':not([data-disabled])[data-open]': `color-mix(in oklab, ${colorVars['--cl-color-negative-subtle']}, ${colorVars['--cl-color-negative']} 8%)`, + ':not([data-disabled]):active': colorVars['--cl-color-negative-alpha-300'], + ':not([data-disabled])[data-open]': colorVars['--cl-color-negative-alpha-300'], '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-negative-subtle'], + ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-negative-alpha-200'], }, }, color: colorVars['--cl-color-negative'], diff --git a/packages/mosaic/src/components/card/card.styles.ts b/packages/mosaic/src/components/card/card.styles.ts index 6fb69152f01..cde201b8a12 100644 --- a/packages/mosaic/src/components/card/card.styles.ts +++ b/packages/mosaic/src/components/card/card.styles.ts @@ -187,7 +187,7 @@ export const footer = stylex.create({ flexShrink: 0, gridTemplateColumns: { [compactCard]: 'minmax(0, 1fr)', default: null }, justifyContent: 'space-between', - borderTopColor: colorVars['--cl-color-border'], + borderTopColor: colorVars['--cl-color-border-subtle'], borderTopStyle: 'solid', borderTopWidth: { default: '0px', @@ -202,7 +202,7 @@ export const branding = stylex.create({ base: { paddingBlock: space['3'], paddingInline: space['6'], - borderBlockStartColor: colorVars['--cl-color-border'], + borderBlockStartColor: colorVars['--cl-color-border-subtle'], borderBlockStartStyle: 'solid', borderBlockStartWidth: '1px', display: 'flex', diff --git a/packages/mosaic/src/components/data-list/data-list.styles.ts b/packages/mosaic/src/components/data-list/data-list.styles.ts index 74a18a3e08c..8241f81c689 100644 --- a/packages/mosaic/src/components/data-list/data-list.styles.ts +++ b/packages/mosaic/src/components/data-list/data-list.styles.ts @@ -4,7 +4,7 @@ import { colorVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../ export const list = stylex.create({ base: { - borderColor: colorVars['--cl-color-border'], + borderColor: colorVars['--cl-color-border-subtle'], borderRadius: radiusVars['--cl-radius-lg'], borderStyle: 'solid', borderWidth: '1px', @@ -25,7 +25,7 @@ export const item = stylex.create({ justifyContent: 'space-between', }, divided: { - borderBlockEndColor: colorVars['--cl-color-border'], + borderBlockEndColor: colorVars['--cl-color-border-subtle'], borderBlockEndStyle: 'solid', borderBlockEndWidth: { default: '1px', diff --git a/packages/mosaic/src/components/drawer/drawer.styles.ts b/packages/mosaic/src/components/drawer/drawer.styles.ts index 0483de18d84..13194785356 100644 --- a/packages/mosaic/src/components/drawer/drawer.styles.ts +++ b/packages/mosaic/src/components/drawer/drawer.styles.ts @@ -140,8 +140,8 @@ export const styles = stylex.create({ // the pointer is captured on it, so `:active` survives the drag. A press elsewhere in the sheet // is not a press on the handle, and leaves it alone. '--_cl-grip-color': { - default: colorVars['--cl-color-border'], - ':active': `color-mix(in oklab, ${colorVars['--cl-color-border']}, ${colorVars['--cl-color-foreground-secondary']} 35%)`, + default: colorVars['--cl-color-border-subtle'], + ':active': `color-mix(in oklab, ${colorVars['--cl-color-border-subtle']}, ${colorVars['--cl-color-foreground-secondary']} 35%)`, }, '--_cl-grip-duration': { default: durationVars['--cl-duration-base'], diff --git a/packages/mosaic/src/components/icon/icon-frame.styles.ts b/packages/mosaic/src/components/icon/icon-frame.styles.ts index aba75af6bb7..a3dd46ebbb4 100644 --- a/packages/mosaic/src/components/icon/icon-frame.styles.ts +++ b/packages/mosaic/src/components/icon/icon-frame.styles.ts @@ -2,6 +2,8 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, radiusVars, space } from '../../tokens.stylex'; +const filledWash = `light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 3.5%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4.5%, transparent))`; + export const styles = stylex.create({ base: { borderColor: 'transparent', @@ -17,10 +19,10 @@ export const styles = stylex.create({ justifyContent: 'center', }, bordered: { - borderColor: colorVars['--cl-color-border'], + borderColor: colorVars['--cl-color-border-subtle'], }, filled: { - backgroundColor: colorVars['--cl-color-neutral-alpha-100'], + backgroundColor: filledWash, }, }); diff --git a/packages/mosaic/src/components/item/item.styles.ts b/packages/mosaic/src/components/item/item.styles.ts index 337e00123f0..2479ff3b614 100644 --- a/packages/mosaic/src/components/item/item.styles.ts +++ b/packages/mosaic/src/components/item/item.styles.ts @@ -58,7 +58,7 @@ export const item = stylex.create({ }, outline: { - borderColor: colorVars['--cl-color-border'], + borderColor: colorVars['--cl-color-border-subtle'], borderStyle: 'solid', borderWidth: '1px', }, diff --git a/packages/mosaic/src/components/menu/menu.styles.ts b/packages/mosaic/src/components/menu/menu.styles.ts index ebe6669f35f..f4fa031f05a 100644 --- a/packages/mosaic/src/components/menu/menu.styles.ts +++ b/packages/mosaic/src/components/menu/menu.styles.ts @@ -180,7 +180,7 @@ export const separator = stylex.create({ // Full-bleed across the popup: cancel the viewport's inline padding. marginBlock: space['0.5'], marginInline: `calc(-1 * ${inset})`, - backgroundColor: colorVars['--cl-color-border'], + backgroundColor: colorVars['--cl-color-border-subtle'], blockSize: '1px', flexShrink: 0, }, diff --git a/packages/mosaic/src/components/pagination/pagination.styles.ts b/packages/mosaic/src/components/pagination/pagination.styles.ts index ec6b3f4930b..fff2941f18d 100644 --- a/packages/mosaic/src/components/pagination/pagination.styles.ts +++ b/packages/mosaic/src/components/pagination/pagination.styles.ts @@ -33,7 +33,7 @@ export const styles = stylex.create({ controls: { gap: space['1'], alignItems: 'center', - borderBlockStartColor: colorVars['--cl-color-border'], + borderBlockStartColor: colorVars['--cl-color-border-subtle'], borderBlockStartStyle: 'solid', borderBlockStartWidth: { [compact]: '1px', default: 0 }, display: { [compact]: 'grid', default: 'flex' }, @@ -55,7 +55,7 @@ export const styles = stylex.create({ whiteSpace: 'nowrap', }, divider: { - backgroundColor: colorVars['--cl-color-border'], + backgroundColor: colorVars['--cl-color-border-subtle'], display: { [compact]: 'none', default: 'block' }, height: '1.25rem', width: '1px', diff --git a/packages/mosaic/src/components/phone-input/phone-input.styles.ts b/packages/mosaic/src/components/phone-input/phone-input.styles.ts index b43a5e4fb63..05889d42676 100644 --- a/packages/mosaic/src/components/phone-input/phone-input.styles.ts +++ b/packages/mosaic/src/components/phone-input/phone-input.styles.ts @@ -51,7 +51,7 @@ export const styles = stylex.create({ width: space['4'], }, divider: { - backgroundColor: colorVars['--cl-color-border'], + backgroundColor: colorVars['--cl-color-border-subtle'], flexShrink: 0, height: space['3.5'], width: '1px', @@ -77,7 +77,7 @@ export const styles = stylex.create({ countrySearchRow: { gap: space['1'], alignItems: 'center', - borderBlockEndColor: colorVars['--cl-color-border'], + borderBlockEndColor: colorVars['--cl-color-border-subtle'], borderBlockEndStyle: 'solid', borderBlockEndWidth: '1px', display: 'flex', diff --git a/packages/mosaic/src/components/profile/profile.styles.ts b/packages/mosaic/src/components/profile/profile.styles.ts index e4383cf69e7..ab853ba81cb 100644 --- a/packages/mosaic/src/components/profile/profile.styles.ts +++ b/packages/mosaic/src/components/profile/profile.styles.ts @@ -60,7 +60,7 @@ export const styles = stylex.create({ // A fixed height so switching pages never resizes the surface. layout: { - borderColor: colorVars['--cl-color-border'], + borderColor: colorVars['--cl-color-border-subtle'], borderRadius: { [compact]: 0, default: radiusVars['--cl-radius-xl'], @@ -129,7 +129,7 @@ export const styles = stylex.create({ nav: { padding: space['4'], - borderInlineEndColor: colorVars['--cl-color-border'], + borderInlineEndColor: colorVars['--cl-color-border-subtle'], borderInlineEndStyle: 'solid', borderInlineEndWidth: '1px', // Hidden in CSS too, so the column never shows on a phone before measuring. diff --git a/packages/mosaic/src/components/section/section.styles.ts b/packages/mosaic/src/components/section/section.styles.ts index 0904539df9f..7841af40df3 100644 --- a/packages/mosaic/src/components/section/section.styles.ts +++ b/packages/mosaic/src/components/section/section.styles.ts @@ -44,7 +44,7 @@ export const styles = stylex.create({ maxWidth: 'none', }, body: { - borderColor: colorVars['--cl-color-border'], + borderColor: colorVars['--cl-color-border-subtle'], borderRadius: radiusVars['--cl-radius-xl'], borderStyle: 'solid', borderWidth: '1px', @@ -56,7 +56,7 @@ export const styles = stylex.create({ }, row: { paddingBlock: space['4'], - borderBlockStartColor: colorVars['--cl-color-border'], + borderBlockStartColor: colorVars['--cl-color-border-subtle'], borderBlockStartStyle: 'solid', borderBlockStartWidth: { default: '1px', @@ -89,7 +89,7 @@ export const styles = stylex.create({ }, nestedItem: { paddingBlock: space['4'], - borderBlockStartColor: colorVars['--cl-color-border'], + borderBlockStartColor: colorVars['--cl-color-border-subtle'], borderBlockStartStyle: 'solid', borderBlockStartWidth: { default: '0px', diff --git a/packages/mosaic/src/components/table/table.styles.ts b/packages/mosaic/src/components/table/table.styles.ts index 01ade1fcb8d..d170e467c79 100644 --- a/packages/mosaic/src/components/table/table.styles.ts +++ b/packages/mosaic/src/components/table/table.styles.ts @@ -30,7 +30,7 @@ export const styles = stylex.create({ search: { maxWidth: '100%', width: '17rem' }, noWrap: { whiteSpace: 'nowrap' }, shell: { - borderColor: colorVars['--cl-color-border'], + borderColor: colorVars['--cl-color-border-subtle'], borderRadius: radiusVars['--cl-radius-xl'], borderStyle: 'solid', borderWidth: '1px', @@ -73,7 +73,7 @@ export const styles = stylex.create({ ':where([data-actionable]:not([data-selected]):hover)': colorVars['--cl-color-surface-subtle'], }, }, - borderBlockStartColor: colorVars['--cl-color-border'], + borderBlockStartColor: colorVars['--cl-color-border-subtle'], borderBlockStartStyle: 'solid', borderBlockStartWidth: '1px', cursor: { default: null, ':where([data-actionable])': 'pointer' }, diff --git a/packages/mosaic/src/components/tag-input/tag-input.styles.ts b/packages/mosaic/src/components/tag-input/tag-input.styles.ts index 466579c0525..e096821816e 100644 --- a/packages/mosaic/src/components/tag-input/tag-input.styles.ts +++ b/packages/mosaic/src/components/tag-input/tag-input.styles.ts @@ -74,7 +74,7 @@ export const styles = stylex.create({ minWidth: 0, }, tagInvalid: { - backgroundColor: colorVars['--cl-color-negative-alpha-100'], + backgroundColor: colorVars['--cl-color-negative-subtle'], color: colorVars['--cl-color-negative'], }, tagLabel: { diff --git a/packages/mosaic/src/components/toast/toast.styles.ts b/packages/mosaic/src/components/toast/toast.styles.ts index bc2e80dfd42..28f2a9dcf57 100644 --- a/packages/mosaic/src/components/toast/toast.styles.ts +++ b/packages/mosaic/src/components/toast/toast.styles.ts @@ -168,11 +168,11 @@ export const styles = stylex.create({ export const iconColors = stylex.create({ success: { - backgroundColor: colorVars['--cl-color-positive-alpha-200'], + backgroundColor: colorVars['--cl-color-positive-subtle'], color: colorVars['--cl-color-positive'], }, error: { - backgroundColor: colorVars['--cl-color-negative-alpha-200'], + backgroundColor: colorVars['--cl-color-negative-subtle'], color: colorVars['--cl-color-negative'], }, loading: { diff --git a/packages/mosaic/src/features/user-profile/user-profile-billing-history-section/user-profile-billing-history-section.styles.ts b/packages/mosaic/src/features/user-profile/user-profile-billing-history-section/user-profile-billing-history-section.styles.ts index 39a2306dbc3..ad6e7402c29 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-billing-history-section/user-profile-billing-history-section.styles.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-billing-history-section/user-profile-billing-history-section.styles.ts @@ -66,7 +66,7 @@ export const styles = stylex.create({ lineHeight: typeScaleVars['--cl-text-xs-leading'], }, pageSizeSelect: { - borderColor: colorVars['--cl-color-border'], + borderColor: colorVars['--cl-color-border-subtle'], borderRadius: radiusVars['--cl-radius-md'], borderStyle: 'solid', borderWidth: '1px', @@ -82,7 +82,7 @@ export const styles = stylex.create({ paddingBlock: space['2'], paddingInline: space['3'], alignItems: 'center', - borderBlockStartColor: colorVars['--cl-color-border'], + borderBlockStartColor: colorVars['--cl-color-border-subtle'], borderBlockStartStyle: 'solid', borderBlockStartWidth: '1px', display: 'flex', @@ -94,7 +94,7 @@ export const styles = stylex.create({ display: 'flex', }, row: { - borderBlockStartColor: colorVars['--cl-color-border'], + borderBlockStartColor: colorVars['--cl-color-border-subtle'], borderBlockStartStyle: 'solid', borderBlockStartWidth: '1px', }, diff --git a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-authenticator.styles.ts b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-authenticator.styles.ts index 790e587ef3c..1bb51ab651e 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-authenticator.styles.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-add-authenticator.styles.ts @@ -8,7 +8,7 @@ export const styles = stylex.create({ }, field: { paddingBlockStart: space['4'], - borderTopColor: colorVars['--cl-color-border'], + borderTopColor: colorVars['--cl-color-border-subtle'], borderTopStyle: 'solid', borderTopWidth: '1px', }, diff --git a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-backup-codes.styles.ts b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-backup-codes.styles.ts index 970f405190d..1db1895fe2f 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-backup-codes.styles.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-mfa-section/user-profile-backup-codes.styles.ts @@ -4,7 +4,7 @@ import { colorVars, radiusVars, space } from '../../../tokens.stylex'; export const styles = stylex.create({ codes: { - borderColor: colorVars['--cl-color-border'], + borderColor: colorVars['--cl-color-border-subtle'], borderRadius: radiusVars['--cl-radius-md'], borderStyle: 'solid', borderWidth: '1px', @@ -15,7 +15,7 @@ export const styles = stylex.create({ listStyleType: 'none', }, cell: { - borderColor: colorVars['--cl-color-border'], + borderColor: colorVars['--cl-color-border-subtle'], borderStyle: 'solid', paddingBlock: space['2'], paddingInline: space['3'], diff --git a/packages/swingset/src/stories/button.mdx b/packages/swingset/src/stories/button.mdx index 294b0718f3d..5e6987da1cd 100644 --- a/packages/swingset/src/stories/button.mdx +++ b/packages/swingset/src/stories/button.mdx @@ -60,7 +60,7 @@ it to show. Every combination below is the same two props. Hover and focus are part of the matrix — hover a cell, or tab through it, to see them. `filled` shifts its fill and `link` underlines. `outline` and `ghost` share the same neutral hover fill, -except `ghost` under `negative`, which is the one cell that tints by color. The focus ring is +except under `negative`, where both tint by color. The focus ring is the same for every cell. Date: Mon, 2 Nov 2026 14:13:26 -0700 Subject: [PATCH 2/3] feat(mosaic): use 12 and 8 percent border washes and fill outline buttons with the surface Co-Authored-By: Claude Sonnet 5.5 --- .changeset/mosaic-color-adoption.md | 2 +- packages/mosaic/ARCHITECTURE.md | 2 +- .../src/components/button/button.styles.ts | 47 +++++++++---------- .../src/components/icon/icon-frame.styles.ts | 4 +- .../src/components/profile/profile.styles.ts | 2 +- packages/mosaic/src/tokens.stylex.ts | 6 +-- 6 files changed, 28 insertions(+), 35 deletions(-) diff --git a/.changeset/mosaic-color-adoption.md b/.changeset/mosaic-color-adoption.md index 8bf389981a3..77abe4bc78a 100644 --- a/.changeset/mosaic-color-adoption.md +++ b/.changeset/mosaic-color-adoption.md @@ -2,4 +2,4 @@ '@clerk/mosaic': patch --- -Controls such as inputs, selects, checkboxes, radios and outline buttons keep the stronger `--cl-color-border`, while containers and dividers (cards, sections, tables, menus, the profile nav) use `--cl-color-border-subtle`. Outline buttons fill with the surface color and lay their hover and pressed tint over it, so the tint works on the page, the surface or a faded fill. Banners, badges, toasts and the tag-input error use the `-subtle` and `-border` tokens, and the neutral badge and banner use `--cl-color-neutral-subtle`. The negative color is slightly deeper in light mode for contrast. +Controls such as inputs, selects, checkboxes, radios and outline buttons keep the stronger `--cl-color-border`, while containers and dividers (cards, sections, tables, menus, the profile nav) use `--cl-color-border-subtle`. The two are now 12 and 8 percent washes in both modes, which makes light-mode controls firmer and leaves containers close to where they were. The profile opened in a dialog draws a border. Outline buttons fill with the surface color, and their hover and pressed states tint that fill. Banners, badges, toasts and the tag-input error use the `-subtle` tokens, and the neutral badge and banner use `--cl-color-neutral-subtle`. diff --git a/packages/mosaic/ARCHITECTURE.md b/packages/mosaic/ARCHITECTURE.md index 34321be5811..c9b4c5e9c30 100644 --- a/packages/mosaic/ARCHITECTURE.md +++ b/packages/mosaic/ARCHITECTURE.md @@ -25,7 +25,7 @@ export const colorVars = stylex.defineVars({ Each exported `*Vars` const in that file is one group (`colorVars`, `radiusVars`, …); the file is the list. `space` is also declared with `defineVars`, but its keys are not `--cl-*` names, so its steps compile to hashed custom properties that are internal, not part of the contract. -There is no gray scale. Every achromatic token (`--cl-color-foreground`, `-border`, `-surface-subtle`, …) is `--cl-color-neutral` (black on light, white on dark) mixed into `--cl-color-surface` at a fixed percentage per mode, so overriding neutral to a warm gray retints the whole ramp. The exception is the borders: `--cl-color-border` (controls) and `--cl-color-border-subtle` (containers and dividers) are neutral washes, 7 and 4 percent in light mode and 12 and 8 percent in dark mode, so they keep the same step over the surface and the faded surface alike. The `--cl--alpha-100|200|300` tokens in `colorVars` are washes (6, 8, and 12 percent of `--cl-color-neutral`, `-negative`, `-positive`, and `-warning` over `transparent`); overriding the solid color retints its washes. +There is no gray scale. Every achromatic token (`--cl-color-foreground`, `-border`, `-surface-subtle`, …) is `--cl-color-neutral` (black on light, white on dark) mixed into `--cl-color-surface` at a fixed percentage per mode, so overriding neutral to a warm gray retints the whole ramp. The exception is the borders: `--cl-color-border` (controls) and `--cl-color-border-subtle` (containers and dividers) are neutral washes, 12 and 8 percent in both modes, so they keep the same step over the surface and the faded surface alike. The `--cl--alpha-100|200|300` tokens in `colorVars` are washes (6, 8, and 12 percent of `--cl-color-neutral`, `-negative`, `-positive`, and `-warning` over `transparent`); overriding the solid color retints its washes. The seeds are `--cl-color-page`, `--cl-color-neutral`, `--cl-color-brand`, `--cl-color-negative`, `--cl-color-positive` and `--cl-color-warning`. Brand and the three statuses share one shape: the seed, `-foreground` (white in light mode; in dark mode the surface color for brand, positive and warning, where the fills are light, and white for negative), `-subtle` (the 8 percent wash, 12 in dark mode) and `-border` (the 12 percent wash), so both work over the page, the surface or a faded fill and `-alpha-100|200|300` washes. Neutral has the same slots under its own names: `--cl-color-surface-subtle`, `--cl-color-neutral-subtle` (the same wash as the accent `-subtle`, for chips such as the neutral badge) and `--cl-color-border`. Only the seeds and the `-foreground` pairs are set by hand, so retinting a seed retints its companions. diff --git a/packages/mosaic/src/components/button/button.styles.ts b/packages/mosaic/src/components/button/button.styles.ts index 9034e26cc8c..df0482309d8 100644 --- a/packages/mosaic/src/components/button/button.styles.ts +++ b/packages/mosaic/src/components/button/button.styles.ts @@ -19,14 +19,14 @@ import { iconScope } from '../icon/icon.markers.stylex'; // Opaque fills blend toward their own on-fill at the same 12%/18%. Neutral has no fill // to blend from, so it rides the opacity ramp above. -// Outline variants keep the surface as their fill and lay the hover and pressed wash over it as an -// inset shadow, so the state tint works on any backdrop and still transitions. +// Outline variants fill with the surface, so their hover and pressed tint mixes the neutral or +// negative color into it. const primaryHover = `color-mix(in oklab, ${colorVars['--cl-color-brand']}, ${colorVars['--cl-color-brand-foreground']} 12%)`; const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-brand']}, ${colorVars['--cl-color-brand-foreground']} 18%)`; -const neutralHoverWash = `inset 0 0 0 100vmax ${colorVars['--cl-color-neutral-alpha-100']}`; -const neutralPressedWash = `inset 0 0 0 100vmax ${colorVars['--cl-color-neutral-alpha-200']}`; -const negativeHoverWash = `inset 0 0 0 100vmax ${colorVars['--cl-color-negative-alpha-200']}`; -const negativePressedWash = `inset 0 0 0 100vmax ${colorVars['--cl-color-negative-alpha-300']}`; +const outlineNeutralHover = `color-mix(in srgb, ${colorVars['--cl-color-neutral']} 6%, ${colorVars['--cl-color-surface']})`; +const outlineNeutralActive = `color-mix(in srgb, ${colorVars['--cl-color-neutral']} 8%, ${colorVars['--cl-color-surface']})`; +const outlineNegativeHover = `color-mix(in srgb, ${colorVars['--cl-color-negative']} 8%, ${colorVars['--cl-color-surface']})`; +const outlineNegativeActive = `color-mix(in srgb, ${colorVars['--cl-color-negative']} 12%, ${colorVars['--cl-color-surface']})`; const negativeHover = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 12%)`; const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']}, ${colorVars['--cl-color-negative-foreground']} 18%)`; @@ -81,7 +81,7 @@ export const styles = stylex.create({ ':not([data-disabled]):active': durationVars['--cl-duration-instant'], ':not([data-disabled]):hover': durationVars['--cl-duration-instant'], }, - transitionProperty: 'background-color, border-color, box-shadow, color, opacity, text-decoration-color', + transitionProperty: 'background-color, border-color, color, opacity, text-decoration-color', // Linear, not `--cl-ease-default`: nothing here moves. An ease on already non-uniform // color interpolation just drags the midpoint, and the house curve's overshoot would // extrapolate past the target color. @@ -187,42 +187,39 @@ export const variants = stylex.create({ // fill, and leaves it independently themeable. 'outline-primary': { borderColor: colorVars['--cl-color-border'], - backgroundColor: colorVars['--cl-color-surface'], - boxShadow: { - default: null, - ':not([data-disabled]):active': neutralPressedWash, - ':not([data-disabled])[data-open]': neutralPressedWash, + backgroundColor: { + default: colorVars['--cl-color-surface'], + ':not([data-disabled]):active': outlineNeutralActive, + ':not([data-disabled])[data-open]': outlineNeutralActive, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralHoverWash, + ':not([data-disabled]):hover:not(:active):not([data-open])': outlineNeutralHover, }, }, color: colorVars['--cl-color-brand'], }, 'outline-neutral': { borderColor: colorVars['--cl-color-border'], - backgroundColor: colorVars['--cl-color-surface'], - boxShadow: { - default: null, - ':not([data-disabled]):active': neutralPressedWash, - ':not([data-disabled])[data-open]': neutralPressedWash, + backgroundColor: { + default: colorVars['--cl-color-surface'], + ':not([data-disabled]):active': outlineNeutralActive, + ':not([data-disabled])[data-open]': outlineNeutralActive, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': neutralHoverWash, + ':not([data-disabled]):hover:not(:active):not([data-open])': outlineNeutralHover, }, }, color: colorVars['--cl-color-foreground'], }, 'outline-negative': { borderColor: colorVars['--cl-color-border'], - backgroundColor: colorVars['--cl-color-surface'], - boxShadow: { - default: null, - ':not([data-disabled]):active': negativePressedWash, - ':not([data-disabled])[data-open]': negativePressedWash, + backgroundColor: { + default: colorVars['--cl-color-surface'], + ':not([data-disabled]):active': outlineNegativeActive, + ':not([data-disabled])[data-open]': outlineNegativeActive, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': negativeHoverWash, + ':not([data-disabled]):hover:not(:active):not([data-open])': outlineNegativeHover, }, }, color: colorVars['--cl-color-negative'], diff --git a/packages/mosaic/src/components/icon/icon-frame.styles.ts b/packages/mosaic/src/components/icon/icon-frame.styles.ts index a3dd46ebbb4..bee50b7ecbe 100644 --- a/packages/mosaic/src/components/icon/icon-frame.styles.ts +++ b/packages/mosaic/src/components/icon/icon-frame.styles.ts @@ -2,8 +2,6 @@ import * as stylex from '@stylexjs/stylex'; import { colorVars, radiusVars, space } from '../../tokens.stylex'; -const filledWash = `light-dark(color-mix(in oklab, ${colorVars['--cl-color-neutral']} 3.5%, transparent), color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4.5%, transparent))`; - export const styles = stylex.create({ base: { borderColor: 'transparent', @@ -22,7 +20,7 @@ export const styles = stylex.create({ borderColor: colorVars['--cl-color-border-subtle'], }, filled: { - backgroundColor: filledWash, + backgroundColor: colorVars['--cl-color-neutral-alpha-100'], }, }); diff --git a/packages/mosaic/src/components/profile/profile.styles.ts b/packages/mosaic/src/components/profile/profile.styles.ts index ab853ba81cb..767b06209f1 100644 --- a/packages/mosaic/src/components/profile/profile.styles.ts +++ b/packages/mosaic/src/components/profile/profile.styles.ts @@ -104,7 +104,6 @@ export const styles = stylex.create({ }, layoutInDialog: { - borderWidth: '0px', blockSize: 'auto', boxShadow: { [compact]: 'none', @@ -120,6 +119,7 @@ export const styles = stylex.create({ layoutFullscreen: { borderRadius: 0, + borderWidth: '0px', boxShadow: 'none', }, diff --git a/packages/mosaic/src/tokens.stylex.ts b/packages/mosaic/src/tokens.stylex.ts index 330ea2788c9..800b359cdea 100644 --- a/packages/mosaic/src/tokens.stylex.ts +++ b/packages/mosaic/src/tokens.stylex.ts @@ -47,10 +47,8 @@ const colorDefaults = { '--cl-color-foreground-secondary': neutralMix(55, 64.25), '--cl-color-foreground-disabled': neutralMix(28.5, 44), - '--cl-color-border': - 'light-dark(color-mix(in oklab, var(--cl-color-neutral) 7%, transparent), var(--cl-color-neutral-alpha-300))', - '--cl-color-border-subtle': - 'light-dark(color-mix(in oklab, var(--cl-color-neutral) 4%, transparent), var(--cl-color-neutral-alpha-200))', + '--cl-color-border': 'var(--cl-color-neutral-alpha-300)', + '--cl-color-border-subtle': 'var(--cl-color-neutral-alpha-200)', '--cl-color-brand': 'light-dark(oklch(0.2046 0 0), oklch(0.9851 0 0))', '--cl-color-brand-foreground': 'light-dark(oklch(1 0 0), var(--cl-color-surface))', From 2f1eddceeb026d927b0f94ac76827f964db10eae Mon Sep 17 00:00:00 2001 From: Preston Booth Date: Mon, 2 Nov 2026 14:16:33 -0700 Subject: [PATCH 3/3] fix(mosaic): keep ghost negative button states unchanged Co-Authored-By: Claude Sonnet 5.5 --- packages/mosaic/src/components/button/button.styles.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/mosaic/src/components/button/button.styles.ts b/packages/mosaic/src/components/button/button.styles.ts index df0482309d8..aff888b7cee 100644 --- a/packages/mosaic/src/components/button/button.styles.ts +++ b/packages/mosaic/src/components/button/button.styles.ts @@ -254,11 +254,11 @@ export const variants = stylex.create({ 'ghost-negative': { backgroundColor: { default: 'transparent', - ':not([data-disabled]):active': colorVars['--cl-color-negative-alpha-300'], - ':not([data-disabled])[data-open]': colorVars['--cl-color-negative-alpha-300'], + ':not([data-disabled]):active': `color-mix(in oklab, ${colorVars['--cl-color-negative-subtle']}, ${colorVars['--cl-color-negative']} 8%)`, + ':not([data-disabled])[data-open]': `color-mix(in oklab, ${colorVars['--cl-color-negative-subtle']}, ${colorVars['--cl-color-negative']} 8%)`, '@media (hover: hover)': { default: null, - ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-negative-alpha-200'], + ':not([data-disabled]):hover:not(:active):not([data-open])': colorVars['--cl-color-negative-subtle'], }, }, color: colorVars['--cl-color-negative'],