Skip to content

fix(cards): stop the glass action bar dimming the action you hover - #6403

Merged
tsahimatsliah merged 7 commits into
mainfrom
claude/action-glassbar-hover-states-84126e
Jul 31, 2026
Merged

fix(cards): stop the glass action bar dimming the action you hover#6403
tsahimatsliah merged 7 commits into
mainfrom
claude/action-glassbar-hover-states-84126e

Conversation

@tsahimatsliah

@tsahimatsliah tsahimatsliah commented Jul 31, 2026

Copy link
Copy Markdown
Member

Changes

Follow-up to #6398. That PR fixed the rest and pressed states of the feed card floating action bar; this fixes the state in between.

#6398 pinned --button-hover-color to the same value as --button-pressed-color so a held pointer couldn't dip below 3:1. Every icon cleared the threshold, so it was never a regression against the original bug — but the accent is always darker than text-primary, so the action under the pointer became the dimmest thing in the bar. The InteractionCounter next to upvote / comment / impressions is text, so it owes SC 1.4.3's 4.5:1, not 3:1 — in light mode all three hovered counters landed at 3.01–3.08. Hover and pressed were also byte-identical, so hovering an already-upvoted action gave no feedback at all.

Hover is now toned per theme through a new --glass-actions-hover-tone:

icon + counter on hover chip behind the button
Dark keeps its accent (bright enough on a near-black bar to read as a lift) accent at 22%
Light text-primary accent at 22%

Icon and counter always share one colour. The pressed state is unchanged.

Implementation notes

  • The light mixin sets --glass-actions-hover-tone: var(--theme-text-primary); the dark mixin sets it to initial (guaranteed-invalid), and each button reads var(--glass-actions-hover-tone, var(--glass-actions-accent)). One fallback instead of a second per-theme colour table.
  • Each button resolves its own --glass-actions-accent and shared rules derive the four state properties from it — the six near-identical mapping blocks are gone, ~100 lines less CSS. Written flat rather than as a parameterised PostCSS mixin: @define-mixin x $arg has no precedent in this repo and three build pipelines would have to agree on it.
  • The chip is the reference, not the bar. Once hover paints a filled surface, that surface is the colour adjacent to the glyph. It matters in dark, where the icon and chip are the same hue, so every point of tint costs the icon contrast — dark copy link had to move cabbage.40 → .10 (2.92 → 3.92) to survive its own 22% chip. Worst remaining cell is downvote at 3.14 on the 32% active step.
  • A pressed button keeps the design system's shallower 12/20% chip: its glyph is back on the accent the chip is tinted with, so the deeper steps would cost contrast for no gain.
  • Two guards were needed. buttons.css resolves hover after [aria-pressed] at equal specificity, so light's neutral hover would otherwise blank the "this is on" accent while the pointer sat on an upvoted action. Both double the class so they outrank the mapping rather than tying with it and depending on block order, and the label needs its own rule because it is a sibling of the button, reading the variable off the .btn-quaternary wrapper.
  • copy-post-btn was a fixed id on three components a feed renders once per card, and QuaternaryButton wires the id to a <label htmlFor>. Now post-scoped in FeedCardGlassActions, ActionButtons and ActionButtons.v2.

Storybook

New page Components / Cards / Glass Actions Hover States, built like the existing theme-compare page: the after columns render the live component with no overrides, the before columns restore #6398's values via scoped CSS, so it stays honest as the component changes. It force-hovers every action (through the .hover hook buttons.css already exposes) so all six states can be read at once, plus a rest → hover → pressed strip and a measured table.

The audit table reads every colour off two live probe pills — accents from --glass-actions-*, fill from --theme-background-default — resolving color-mix() through a canvas pixel, so it can't drift from the stylesheet. The WCAG math moved to glassContrast.ts, shared with the theme-compare page.

Known follow-up

#6405 — light-mode pressed counters are text at 3.01–3.08 against a 4.5:1 threshold. Pre-existing (it arrived with #6398 and is unchanged here), and both fixes are design calls: darkening the light pressed accents, or giving pressed counters the neutral treatment hover now uses.

Events

No.

Experiment

No.

Manual Testing

Storybook only — the bar has no dedicated route, and hover can't be exercised by the unit tests.

  • Read computed colours for all six actions × before/after × dark/light, confirming icon and counter match in every cell and that the pressed accent survives hover.
  • Theme matrix: html.light, html.dark, html.auto with an OS light preference, and .invert subtrees.
  • pnpm --filter @dailydotdev/shared test src/components/cards — 30 suites / 217 tests
  • pnpm --filter webapp test — 45 suites / 336 tests
  • node ./scripts/typecheck-strict-changed.js

Note

.btn and .btn-quaternary label transition color over 150ms, so getComputedStyle right after a class or theme change returns the mid-transition value. Disable transitions before verifying this page by hand.

On those affected packages:

  • Have you done sanity checks in the webapp? (via Storybook, which renders the shared component)
  • Have you done sanity checks in the extension?
  • Does this not break anything in companion?

CSS-only change to an existing component, scoped to .feed-card-glass-actions; the extension and companion selector groups are untouched.

🤖 Generated with Claude Code

Preview domain

https://claude-action-glassbar-hover-sta.preview.app.daily.dev

The accessibility fix in #6398 pinned `--button-hover-color` to the same
value as `--button-pressed-color` so a held pointer couldn't dip below
3:1. Every icon cleared the threshold, but the accent is always darker
than `text-primary`, so the action under the pointer became the dimmest
thing in the bar — and the `InteractionCounter` beside it is text, owing
SC 1.4.3's 4.5:1 rather than 3:1. In light mode all three counters landed
at 3.01-3.08. Hover and pressed were also byte-identical, so hovering an
already-upvoted action did nothing at all.

Hover is now toned per theme through `--glass-actions-hover-tone`:

  dark  content keeps its accent (5.34-11.16 — bright enough on a
        near-black bar that it still reads as a lift)
  light content goes to `text-primary` (15.03), and the accent moves
        into the surface instead
  both  the accent sits at 22% behind the button rather than the design
        system's 12%, and active deepens to 32% (the ladder's own active
        background is 20%, which would have stepped backwards from a 22%
        hover). Icon and counter always share one colour.

The pressed state is untouched. One guard was needed: `buttons.css`
resolves hover AFTER `aria-pressed` at equal specificity, so light's
neutral hover would otherwise blank the "this is on" accent for as long
as the pointer sat on an upvoted action. The counter needs its own rule
because it is a sibling of the button and reads the variable off the
`.btn-quaternary` wrapper.

Copy link regains a `.btn-tertiary-cabbage` block — dropped in the #6398
review as a no-op — purely for the surface and the light hover tone; it
still sets no pressed colour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
daily-webapp Ready Ready Preview Jul 31, 2026 12:49pm
storybook Building Building Preview Jul 31, 2026 12:49pm

Request Review

tsahimatsliah and others added 2 commits July 31, 2026 15:17
Review of the hover change caught a regression it introduced, plus a
documentation error behind it.

Raising the hover surface from the design system's 12% to 22% changes
what the icon is measured against. At 12% the chip is invisible enough
that the bar is the honest reference; at 22% the chip is the colour
adjacent to the glyph, and in dark the icon and chip are the same hue —
so every point of tint costs the icon contrast. Dark copy link went from
3.31 to 2.92 (2.55 at the 32% active step), the only cell the change
moved from pass to fail. cabbage.40 -> .10 takes it to 3.92 / 3.24, and
the worst remaining dark cell is downvote at 3.14.

A pressed button now keeps the shallower 12/20% chip. Its glyph is back
on the accent the chip is tinted with, so the deeper steps cost contrast
for no gain — light pressed-and-hovered returns to 2.65-2.74, exactly
where main has it, rather than the 2.37-2.47 the deeper chip caused. The
residual miss is a palette limit, not a regression.

Also in this pass:

- The six mapping blocks were identical bar a variable name. Each button
  now resolves `--glass-actions-accent` and shared rules turn that into
  the state properties, which is ~100 lines less CSS.
- `copy-post-btn` was a fixed id on a component a feed renders once per
  card, and QuaternaryButton wires the id to a `<label htmlFor>`. Now
  post-scoped.
- Comments across all four files cut back to the constraints that aren't
  visible in the code. The measured ratios belong in the Storybook page
  that computes them live, not in a stylesheet where they go stale
  silently.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@tsahimatsliah tsahimatsliah left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Good, tightly-scoped follow-up. The core diagnosis is right — pinning --button-hover-color to the pressed value in #6398 made the hovered action the dimmest thing in the bar, and hover became indistinguishable from pressed. Routing everything through one --glass-actions-accent per button and deriving the four state properties from it removes ~100 lines of near-duplicate CSS, and the second pass catching that a 22% chip changes what the icon is measured against (copy link 3.31 → 2.92) is exactly the right instinct.

I verified the mechanics rather than the numbers:

  • --button-hover-color / --button-active-color / --button-hover-background / --button-active-background all exist and are consumed by buttons.css (.btn &:hover, &.hover and &:active, &.active), and &[aria-pressed="true"] does resolve before the hover block at equal specificity — so the guard rules are genuinely needed.
  • The sibling-label guard is needed too: .btn-quaternary & :hover ~ label (0,2,0) is declared after & [aria-pressed="true"] ~ label, and the new rule at 0,3,1 is what hands the pressed colour back.
  • --glass-actions-hover-tone: initial is correct per spec — a custom property set to initial is guaranteed-invalid, so var(--x, <accent>) takes the fallback. It also survives nested .invert, since the value is assigned on the pill itself.
  • Import order is fine: components.css pulls buttons.css before feedCardGlassActions.css.
  • Bookmark does carry post-${post.id}-bookmark-btn (via buttonProps), so the story's [id$='-bookmark-btn'] row is live.
  • CI is green.

No blocking findings. A few things worth resolving or filing before merge.

Non-blocking

1. The pressed counter in light mode is still text below 4.5:1 — and this PR is the one that established that threshold. The description frames the residual light pressed miss (2.65–2.74) as an icon/palette limit, but Upvote, Comment and Impressions carry an InteractionCounter, and the new pressed-label guard hands that label the pressed accent — which is mixed down to ~3.0 against the bar by design. So a light-mode upvoted count is text at roughly 2.7–3.0 against SC 1.4.3's 4.5. That is pre-existing rather than introduced here, but it is the same defect class the PR is fixing, and a paragraph in a PR description is not where it will get found again. Either give pressed counters the neutral treatment hover now uses, or file the "darken the light pressed accents" follow-up as a ticket and link it.

2. The contrast audit table hard-codes the accent hexes. The visual columns read var(--glass-actions-*) off the pill, which is what keeps them honest — but themes[].accents / afterAccents / fill in GlassActionsHoverStates.stories.tsx are literal hexes (#57e087, #039750, #d97efe, #0f1218, …). They are correct today (I checked cabbage.10 = #D97EFE and light accent-cabbage-default = cabbage.60 = #A641CC), but this is the exact staleness the PR cites as the reason for deleting the ratios from the stylesheet — just moved one file over, where nothing flags it either. getComputedStyle(pill).getPropertyValue('--glass-actions-upvote') would make the table as live as the columns above it.

3. The pressed guard wins on source order at equal specificity. html .feed-card-glass-actions .btn[aria-pressed='true'] and the mapping block are both 0,3,1, so reordering the file silently disables the "keep the pressed accent while hovered" behaviour with no build or test signal. Doubling the class as the mapping already does (.feed-card-glass-actions.feed-card-glass-actions) makes it 0,4,1 and order-independent, and the story's own beforeCss uses that trick for the same reason.

4. The id fix is incomplete. ActionButtons.tsx:281 and ActionButtons.v2.tsx:227 still use a fixed id="copy-post-btn", and both are feed-card components rendered once per card — the same duplicate-id situation this PR just fixed in FeedCardGlassActions, with the same <label htmlFor> wiring in QuaternaryButton. Two one-line changes, or a follow-up ticket.

5. The label guard is unconditional. … .btn-quaternary [aria-pressed='true'] ~ label { color: var(--button-pressed-color) } applies in every state at 0,3,1, so it also outranks & [disabled] ~ label (0,2,0) for a pressed-and-disabled counter. Probably unreachable for these six actions, but scoping it to :hover/:active — the states it exists to fix — would keep it from quietly owning the label's colour outright.

6. The 600-line "before" half of the story has a short shelf life. Restoring #6398's values via scoped CSS is a nice way to make the argument, but once this merges the before columns document a state that no longer exists anywhere, and nothing validates that they still reproduce it. Worth deciding now whether the page keeps them permanently or is trimmed to the live matrix plus the measured table after review.

Verification

  • Read the PR review guide and code-style rules
  • Traced the cascade end to end in buttons.css (hover / active / pressed blocks, .btn-quaternary label rules) and confirmed the specificity and source-order claims in the description
  • Confirmed the initial guaranteed-invalid fallback and its behaviour under nested .invert
  • Checked the story's forced-hover selectors against the component's real ids
  • Spot-checked the hard-coded audit hexes against tailwind/colors.ts
  • CI inspected — green
  • Ratios not independently recomputed; taking the Storybook page's math as given
  • Not verified in the extension or companion — the description flags both as unchecked, and while the selector groups are untouched, the companion wrapper is in those selector lists

Verdict

Comment — nothing blocking. Item 1 deserves a decision (fix or ticket) and items 3 and 4 are cheap enough to fold in.

Reviewed by AI.

Comment thread packages/shared/src/styles/components/feedCardGlassActions.css Outdated
Comment thread packages/shared/src/styles/components/feedCardGlassActions.css Outdated
Comment thread packages/storybook/stories/components/cards/GlassActionsHoverStates.stories.tsx Outdated
Addresses the review on #6403.

- The pressed guard tied with the mapping block at 0,3,1 and won only on
  source order, so reordering the file would have silently dropped the
  "keep the pressed accent while hovered" behaviour with no build or test
  signal. Doubling the class makes it 0,4,1.
- The label guard applied in every state, which also outranked
  `.btn-quaternary [disabled] ~ label`. Scoped to the hover/active states
  it exists to correct.
- `ActionButtons.tsx` and `ActionButtons.v2.tsx` still carried the fixed
  `id="copy-post-btn"` this PR had already fixed in FeedCardGlassActions.
  Both are feed-card components rendered once per card, and
  QuaternaryButton wires the id to a `<label htmlFor>`.
- The story's contrast audit restated the accents as literal hexes — the
  same staleness this PR cites when taking the ratios out of the
  stylesheet, just moved one file over. It now reads every colour off two
  live pills, resolving `color-mix()` through a canvas pixel, and detects
  the per-theme hover tone through the same `var()` fallback the shipped
  mapping uses.

The remaining review item — light-mode pressed counters sitting at
3.01-3.08 against a 4.5:1 threshold — is pre-existing and needs a design
call, so it is filed as #6405 rather than folded in here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tsahimatsliah

Copy link
Copy Markdown
Member Author

All six items addressed in 6aa7266 — four fixed, one filed, one decided.

# Item Resolution
1 Light pressed counter below 4.5:1 Filed as #6405 — pre-existing, needs a design call
2 Audit table hard-codes accent hexes Fixed — reads live values off two probe pills
3 Pressed guard wins on source order Fixed — doubled class, now 0,4,1
4 id fix incomplete Fixed — ActionButtons.tsx + .v2.tsx post-scoped
5 Label guard unconditional Fixed — scoped to :hover / .hover / :active
6 "Before" half of the story Decided — kept, see below

On (1). Filed rather than folded in because both exits are design calls, not mechanical fixes: darkening the light pressed accents changes a look signed off against the 90% bar, and giving pressed counters the neutral hover treatment breaks the icon-and-counter-share-one-colour pairing that was explicitly requested for pressed. #6405 records the measured numbers (3.01–3.08 against the bar, 2.65–2.74 against the chip) and both options.

On (2) — worth calling out, because your framing was sharper than the fix I'd have written. The audit now renders two zero-size probe pills (one plain, one .invert) and reads accents from --glass-actions-*, fill from --theme-background-default, rest from --theme-text-primary. Resolution goes through a canvas pixel, since getComputedStyle returns color-mix(…) / oklab(…) rather than sRGB. Whether a theme keeps its accent on hover is detected via the same var(--glass-actions-hover-tone, <sentinel>) fallback the shipped mapping uses, so the page asserts nothing about the CSS that the CSS doesn't tell it. Every number came out identical to the hard-coded version.

On (6) — keeping the before columns. Two reasons. It matches the merged "Glass Actions Theme Compare" page, which keeps its own before/after for the #6398 change, so trimming this one would leave the pair inconsistent. And the restoration is not frozen the way a screenshot would be: every before column reads var(--glass-actions-*) (or var(--theme-accent-cabbage-default) for copy link, whose dark accent this PR moved), so five of six track the tokens automatically. The only pinned values are the 12% / 20% tint percentages, which is what "before" means. Happy to trim to the live matrix plus the audit if you'd rather.

On the two unchecked verification boxes. Extension and companion are still unverified by me — the selector groups are untouched, including the #daily-companion-wrapper entries, and the new rules sit inside the same html … mapping, so the risk is low; but I don't want to claim more than I checked.

Re-verified after the changes: shared cards 30 suites / 217 tests, webapp 45 / 336, changed-file strict typecheck, and the rendered matrix in both themes with transitions disabled.

One methodology note that cost me real time and is worth recording for anyone verifying this page by hand: .btn and .btn-quaternary label transition color over 150ms, so getComputedStyle immediately after a class or theme change returns the mid-transition value. It made a correct guard look broken twice. Kill transitions first, or read in a later tick.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tsahimatsliah
tsahimatsliah merged commit 3a79ca7 into main Jul 31, 2026
13 checks passed
@tsahimatsliah
tsahimatsliah deleted the claude/action-glassbar-hover-states-84126e branch July 31, 2026 12:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants