From af96cec1a738e8d043bc6befd175226da43ab913 Mon Sep 17 00:00:00 2001 From: Max Yinger Date: Thu, 30 Jul 2026 15:22:08 -0600 Subject: [PATCH] fix(ui): correct Mosaic Item pressed state and color transitions The pressed background on interactive rows never rendered on hover-capable devices. StyleX doubles an atom's class inside an at-rule, so the `@media (hover: hover)` `:hover` rule compiled to a higher specificity than the bare `:active` rule and won for the duration of the press. Excluding `:active` from the hover selector stops it matching while pressed, matching what `button` already does. Rows also had no transition, so the hover background snapped. They now use the same instant-press/soft-settle treatment as `button`, gated on a bare `:active` rather than `:enabled:active` because a row renders as a `div` or an `a` and `:enabled` only matches form controls. `Item.Title` gets a matching `color` transition so its hover shift settles in step with the row behind it. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/mosaic-item-press-state.md | 2 ++ .../src/mosaic/components/item/item.styles.ts | 23 +++++++++++++++++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 .changeset/mosaic-item-press-state.md diff --git a/.changeset/mosaic-item-press-state.md b/.changeset/mosaic-item-press-state.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-item-press-state.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/ui/src/mosaic/components/item/item.styles.ts b/packages/ui/src/mosaic/components/item/item.styles.ts index 9b83d54743d..99119f38c28 100644 --- a/packages/ui/src/mosaic/components/item/item.styles.ts +++ b/packages/ui/src/mosaic/components/item/item.styles.ts @@ -1,6 +1,6 @@ import * as stylex from '@stylexjs/stylex'; -import { colorVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; +import { colorVars, durationVars, fontWeightVars, radiusVars, space, typeScaleVars } from '../../tokens.stylex'; export const item = stylex.create({ base: { @@ -30,11 +30,24 @@ export const item = stylex.create({ backgroundColor: { default: null, ':active': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 8%, transparent)`, + // Hover excludes `:active` explicitly — StyleX doubles the class inside an at-rule, so a + // `@media (hover: hover)` `:hover` outranks the bare `:active` and wins while pressing. '@media (hover: hover)': { - ':hover': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`, + default: null, + ':hover:not(:active)': `color-mix(in oklab, ${colorVars['--cl-color-neutral']} 4%, transparent)`, }, }, cursor: 'pointer', + // The press reads as contact, not a fade, so it lands instantly; release falls back to `fast`. + // Bare `:active`, not `:enabled:active` as on `button` — the row renders as a `div` or an `a`, + // and `:enabled` only ever matches form controls. + transitionDuration: { + default: durationVars['--cl-duration-fast'], + ':active': durationVars['--cl-duration-instant'], + }, + transitionProperty: 'background-color', + // Linear, not `--cl-ease-default`: nothing here moves. + transitionTimingFunction: 'linear', }, // vertical density derived from the variant; horizontal padding is shared by the base @@ -79,6 +92,12 @@ export const title = stylex.create({ fontSize: typeScaleVars['--cl-text-sm-size'], fontWeight: fontWeightVars['--cl-font-medium'], lineHeight: typeScaleVars['--cl-text-sm-leading'], + // Matches the row's background fade so the two settle together. No `:active` branch: the row + // still matches `:hover` while pressed, so the title's color doesn't change on press. + transitionDuration: durationVars['--cl-duration-fast'], + transitionProperty: 'color', + // Linear, not `--cl-ease-default`: nothing here moves. + transitionTimingFunction: 'linear', }, });