@@ -36,6 +36,18 @@ const primaryActive = `color-mix(in oklab, ${colorVars['--cl-color-primary']}, $
3636const negativeHover = `color-mix(in oklab, ${ colorVars [ '--cl-color-negative' ] } , ${ colorVars [ '--cl-color-negative-foreground' ] } 12%)` ;
3737const negativeActive = `color-mix(in oklab, ${ colorVars [ '--cl-color-negative' ] } , ${ colorVars [ '--cl-color-negative-foreground' ] } 18%)` ;
3838
39+ // An icon rests a step below its label and comes up to it on hover, so the label leads and the icon
40+ // reads as supporting. Each is an opaque faded form of the cell's own text color, not an alpha of
41+ // it: a translucent icon would pick up whatever sits behind the button and drift per surface.
42+ //
43+ // The achromatic foregrounds (`primary`, `neutral-foreground`) share the house faded gray. The two
44+ // that carry hue fade toward the tint that belongs to them, and the light-on-fill pairs fade toward
45+ // their own fill, which is the only backdrop they can ever sit on.
46+ const iconFadedNeutral = colorVars [ '--cl-color-neutral-faded' ] ;
47+ const iconFadedNegative = `color-mix(in oklab, ${ colorVars [ '--cl-color-negative' ] } , ${ colorVars [ '--cl-color-negative-faded' ] } 50%)` ;
48+ const iconFadedOnPrimary = `color-mix(in oklab, ${ colorVars [ '--cl-color-primary-foreground' ] } , ${ colorVars [ '--cl-color-primary' ] } 40%)` ;
49+ const iconFadedOnNegative = `color-mix(in oklab, ${ colorVars [ '--cl-color-negative-foreground' ] } , ${ colorVars [ '--cl-color-negative' ] } 40%)` ;
50+
3951// Interactive states are gated on `:enabled`: the `disabled` attribute blocks activation but
4052// not matching, and the button stays hit-testable so `cursor: not-allowed` renders and a
4153// wrapping tooltip still gets the pointer. Disabled keeps its resting fill and only dims.
@@ -53,18 +65,6 @@ const negativeActive = `color-mix(in oklab, ${colorVars['--cl-color-negative']},
5365
5466export const styles = stylex . create ( {
5567 base : {
56- // The icon rides at reduced strength until the button is hovered, so it reads as secondary to
57- // the label at rest. Mixed from `currentColor` rather than set per cell: every variant×color
58- // already carries its own foreground, so one declaration covers the whole matrix. `Icon` reads
59- // this var (`icon.styles.ts`); StyleX can't emit a descendant rule, so the value has to cross
60- // the element boundary as a custom property.
61- '--_cl-icon-color' : {
62- default : `color-mix(in oklab, currentColor 60%, transparent)` ,
63- '@media (hover: hover)' : {
64- default : null ,
65- ':enabled:hover' : 'currentColor' ,
66- } ,
67- } ,
6868 borderColor : 'transparent' ,
6969 borderRadius : radiusVars [ '--cl-radius-md' ] ,
7070 borderStyle : 'solid' ,
@@ -145,9 +145,22 @@ export const styles = stylex.create({
145145// variant × color, one entry per cell of the design matrix, keyed `<variant>-<color>` so the
146146// component can index directly. Each cell is self-contained so it reads — and tunes — against
147147// the spec without tracing shared parts.
148+ //
149+ // `--_cl-icon-color` lives per cell rather than once in `base`: StyleX resolves a property to the
150+ // last style that declares it, so a cell setting it would drop `base`'s hover branch wholesale
151+ // rather than merge with it. `Icon` reads the var (`icon.styles.ts`) — StyleX can't emit a
152+ // descendant rule, so the value crosses the element boundary as a custom property.
148153export const variants = stylex . create ( {
149154 // The pressed state stays outside the hover media query so no-hover devices still get one.
150155 'filled-primary' : {
156+ '--_cl-icon-color' : {
157+ default : iconFadedOnPrimary ,
158+ ':enabled[data-open]' : colorVars [ '--cl-color-primary-foreground' ] ,
159+ '@media (hover: hover)' : {
160+ default : null ,
161+ ':enabled:hover' : colorVars [ '--cl-color-primary-foreground' ] ,
162+ } ,
163+ } ,
151164 backgroundColor : {
152165 default : colorVars [ '--cl-color-primary' ] ,
153166 ':enabled:active' : primaryActive ,
@@ -160,6 +173,14 @@ export const variants = stylex.create({
160173 color : colorVars [ '--cl-color-primary-foreground' ] ,
161174 } ,
162175 'filled-neutral' : {
176+ '--_cl-icon-color' : {
177+ default : iconFadedNeutral ,
178+ ':enabled[data-open]' : colorVars [ '--cl-color-neutral-foreground' ] ,
179+ '@media (hover: hover)' : {
180+ default : null ,
181+ ':enabled:hover' : colorVars [ '--cl-color-neutral-foreground' ] ,
182+ } ,
183+ } ,
163184 backgroundColor : {
164185 default : neutralStep0 ,
165186 ':enabled:active' : neutralStep2 ,
@@ -172,6 +193,14 @@ export const variants = stylex.create({
172193 color : colorVars [ '--cl-color-neutral-foreground' ] ,
173194 } ,
174195 'filled-negative' : {
196+ '--_cl-icon-color' : {
197+ default : iconFadedOnNegative ,
198+ ':enabled[data-open]' : colorVars [ '--cl-color-negative-foreground' ] ,
199+ '@media (hover: hover)' : {
200+ default : null ,
201+ ':enabled:hover' : colorVars [ '--cl-color-negative-foreground' ] ,
202+ } ,
203+ } ,
175204 backgroundColor : {
176205 default : colorVars [ '--cl-color-negative' ] ,
177206 ':enabled:active' : negativeActive ,
@@ -188,6 +217,14 @@ export const variants = stylex.create({
188217 // rises underneath it. Keeps the border opaque so it can't alpha-fade against an incoming
189218 // fill, and leaves it independently themeable.
190219 'outline-primary' : {
220+ '--_cl-icon-color' : {
221+ default : iconFadedNeutral ,
222+ ':enabled[data-open]' : colorVars [ '--cl-color-primary' ] ,
223+ '@media (hover: hover)' : {
224+ default : null ,
225+ ':enabled:hover' : colorVars [ '--cl-color-primary' ] ,
226+ } ,
227+ } ,
191228 borderColor : colorVars [ '--cl-color-border' ] ,
192229 backgroundColor : {
193230 default : 'transparent' ,
@@ -201,6 +238,14 @@ export const variants = stylex.create({
201238 color : colorVars [ '--cl-color-primary' ] ,
202239 } ,
203240 'outline-neutral' : {
241+ '--_cl-icon-color' : {
242+ default : iconFadedNeutral ,
243+ ':enabled[data-open]' : colorVars [ '--cl-color-neutral-foreground' ] ,
244+ '@media (hover: hover)' : {
245+ default : null ,
246+ ':enabled:hover' : colorVars [ '--cl-color-neutral-foreground' ] ,
247+ } ,
248+ } ,
204249 borderColor : colorVars [ '--cl-color-border' ] ,
205250 backgroundColor : {
206251 default : 'transparent' ,
@@ -214,6 +259,14 @@ export const variants = stylex.create({
214259 color : colorVars [ '--cl-color-neutral-foreground' ] ,
215260 } ,
216261 'outline-negative' : {
262+ '--_cl-icon-color' : {
263+ default : iconFadedNegative ,
264+ ':enabled[data-open]' : colorVars [ '--cl-color-negative' ] ,
265+ '@media (hover: hover)' : {
266+ default : null ,
267+ ':enabled:hover' : colorVars [ '--cl-color-negative' ] ,
268+ } ,
269+ } ,
217270 borderColor : colorVars [ '--cl-color-border' ] ,
218271 backgroundColor : {
219272 default : 'transparent' ,
@@ -228,6 +281,14 @@ export const variants = stylex.create({
228281 } ,
229282
230283 'ghost-primary' : {
284+ '--_cl-icon-color' : {
285+ default : iconFadedNeutral ,
286+ ':enabled[data-open]' : colorVars [ '--cl-color-primary' ] ,
287+ '@media (hover: hover)' : {
288+ default : null ,
289+ ':enabled:hover' : colorVars [ '--cl-color-primary' ] ,
290+ } ,
291+ } ,
231292 backgroundColor : {
232293 default : 'transparent' ,
233294 ':enabled:active' : neutralStep1 ,
@@ -240,6 +301,14 @@ export const variants = stylex.create({
240301 color : colorVars [ '--cl-color-primary' ] ,
241302 } ,
242303 'ghost-neutral' : {
304+ '--_cl-icon-color' : {
305+ default : iconFadedNeutral ,
306+ ':enabled[data-open]' : colorVars [ '--cl-color-neutral-foreground' ] ,
307+ '@media (hover: hover)' : {
308+ default : null ,
309+ ':enabled:hover' : colorVars [ '--cl-color-neutral-foreground' ] ,
310+ } ,
311+ } ,
243312 backgroundColor : {
244313 default : 'transparent' ,
245314 ':enabled:active' : neutralStep1 ,
@@ -254,6 +323,14 @@ export const variants = stylex.create({
254323 // The one ghost that tints instead of graying, so its pressed step walks its own faded
255324 // fill toward the negative it carries rather than joining the gray ramp.
256325 'ghost-negative' : {
326+ '--_cl-icon-color' : {
327+ default : iconFadedNegative ,
328+ ':enabled[data-open]' : colorVars [ '--cl-color-negative' ] ,
329+ '@media (hover: hover)' : {
330+ default : null ,
331+ ':enabled:hover' : colorVars [ '--cl-color-negative' ] ,
332+ } ,
333+ } ,
257334 backgroundColor : {
258335 default : 'transparent' ,
259336 ':enabled:active' : `color-mix(in oklab, ${ colorVars [ '--cl-color-negative-faded' ] } , ${ colorVars [ '--cl-color-negative' ] } 8%)` ,
@@ -269,6 +346,13 @@ export const variants = stylex.create({
269346 // link opts out of the box the size axis sets — it reads as text, not a control. Per-side
270347 // zeros for the same reason `shapeSquare` uses them.
271348 'link-primary' : {
349+ '--_cl-icon-color' : {
350+ default : iconFadedNeutral ,
351+ '@media (hover: hover)' : {
352+ default : null ,
353+ ':enabled:hover' : colorVars [ '--cl-color-primary' ] ,
354+ } ,
355+ } ,
272356 backgroundColor : 'transparent' ,
273357 color : colorVars [ '--cl-color-primary' ] ,
274358 paddingInlineEnd : 0 ,
@@ -278,6 +362,13 @@ export const variants = stylex.create({
278362 height : 'auto' ,
279363 } ,
280364 'link-neutral' : {
365+ '--_cl-icon-color' : {
366+ default : iconFadedNeutral ,
367+ '@media (hover: hover)' : {
368+ default : null ,
369+ ':enabled:hover' : colorVars [ '--cl-color-neutral-foreground' ] ,
370+ } ,
371+ } ,
281372 backgroundColor : 'transparent' ,
282373 color : colorVars [ '--cl-color-neutral-foreground' ] ,
283374 paddingInlineEnd : 0 ,
@@ -287,6 +378,13 @@ export const variants = stylex.create({
287378 height : 'auto' ,
288379 } ,
289380 'link-negative' : {
381+ '--_cl-icon-color' : {
382+ default : iconFadedNegative ,
383+ '@media (hover: hover)' : {
384+ default : null ,
385+ ':enabled:hover' : colorVars [ '--cl-color-negative' ] ,
386+ } ,
387+ } ,
290388 backgroundColor : 'transparent' ,
291389 color : colorVars [ '--cl-color-negative' ] ,
292390 paddingInlineEnd : 0 ,
0 commit comments