Skip to content

feat: SW-2445 Button/ButtonGroup selected state (aria-pressed) - #195

Merged
boramyi-ts merged 12 commits into
mainfrom
SW-2445-selected-state
Sep 10, 2026
Merged

feat: SW-2445 Button/ButtonGroup selected state (aria-pressed)#195
boramyi-ts merged 12 commits into
mainfrom
SW-2445-selected-state

Conversation

@boramyi-ts

@boramyi-ts boramyi-ts commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

SW-2445 — Button / ButtonGroup selected state (aria-pressed)

Scope: Button/ButtonGroup. The Toggle/ToggleGroup work lives in #196 (SW-2563).
Update: the shared --selected tokens now land here, not in #196 — see "Token ownership" below. This PR no longer depends on #196.

What & why

aria-pressed on a Button had no visual state, so a segmented button group couldn't show which option was active. This adds a selected state so toggle-style button groups read correctly.

  • Button: aria-pressed now gets the shared selected treatment — soft --selected tint + primary text + pressed inset — scoped to the outline and ghost variants (the ones used for toggle/segmented groups). Filled variants (default/secondary/destructive) keep their own background, so an active secondary button (e.g. AssistantDockControls) is unaffected.
  • No selected border is applied: ButtonGroup collapses each non-first button's left border, so a colored selected border would only cover 3 sides and look asymmetric. The tint + pressed inset carries the state; segment borders stay uniform.
  • ButtonGroup "Variations" story (SW-T5648): command group (no selection), single-active (aria-pressed), toggle-buttons (multiple aria-pressed), text + separator, vertical, and vertical + separator.

Token ownership (changed from the original plan)

The --selected / --selected-foreground / --selected-border / --shadow-inset-pressed tokens are defined in this PR's src/index.tailwind.css.

They were originally kept in #196, but Tailwind v4 emits no CSS at all for a utility whose theme key is missing — it does not warn or fail. Merging this PR first would therefore have shipped an aria-pressed state that renders nothing, with all CI checks green. Defining them here removes the merge-order dependency entirely.

#196 follow-up: #196 still adds the same four token lines, so the two PRs will conflict in src/index.tailwind.css. Resolve by dropping them from #196 and keeping this PR's copy — the values are byte-identical, so nothing else in #196 changes.

Also in this PR: ButtonGroupText / ButtonGroupSeparator restyle

Not strictly part of the aria-pressed work, but included because the separator change is what makes selected segments line up. Calling it out because it changes existing screens:

  • ButtonGroupText: drops bg-muted, font-mediumfont-normal, adds text-muted-foreground. The text chip now reads as a label rather than a filled segment.
  • ButtonGroupSeparator: bg-inputbg-border, drops the mx-px/my-px inset so the divider sits flush. Paired with a new rule that strips the preceding child's border, so you get one clean 1px divider instead of a doubled line.

Known visual impact: PlateZoomControl — the zoom percentage readout loses its muted chip fill. MessageBranchPage already overrides these classes and is unaffected. Both worth an eyeball on the preview before merge.

Selection semantics

ButtonGroup stays presentational (no built-in selection) — you mark the active child aria-pressed. For true single/multi-select semantics, ToggleGroup (#196) remains the purpose-built option.

Tests

Button and ButtonGroup play tests; typecheck + lint clean. The Variations story now asserts the selected tint actually resolves to a paint and differs from an unpressed sibling — so if the tokens ever go missing again, the test fails instead of silently rendering nothing.

🤖 Generated with Claude Code

Replace the solid-primary selected fill with one consistent state language —
a soft tint (--selected), primary text, a pressed inset, segment dividers, and
an always-outlined segmented container — so selection reads without relying on
an unselected neighbour and 'all selected' no longer collapses into one button.

- tokens: --selected / --selected-foreground / --selected-border /
  --shadow-inset-pressed (derived from --primary/--foreground; theme-tracking),
  exposed as bg-selected / text-selected-foreground / border-selected-border.
- Toggle: on-state now tint + pressed (was bg-accent); outline gets a
  selected border.
- ToggleGroup: drop the SW-2292 solid override (base now tints); segmented
  groups are always outlined with dividers between items (fixes default-variant
  having no border); new ToggleGroupItem selectedIndicator="dot" renders a
  dotted-ring resting affordance that cross-fades to a check (fixed-width slot,
  no layout shift on click).
- Button: aria-pressed now styled with the same selected language, so a
  segmented button group can show its active item (previously aria-pressed had
  no visual state).
- story + play test for the selected indicator; new story testCaseId left "".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@boramyi-ts
boramyi-ts requested review from a team as code owners August 28, 2026 20:52
@vercel

vercel Bot commented Aug 28, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated
ts-lib-ui-kit-storybook Ready Ready Preview Sep 3, 2026 1:35pm UTC

Request Review

@unblocked unblocked Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found.

About Unblocked

Unblocked has been set up to automatically review your team's pull requests to identify genuine bugs and issues.

📖 Documentation — Learn more in our docs.

💬 Ask questions — Mention @unblocked to request a review or summary, or ask follow-up questions.

👍 Give feedback — React to comments with 👍 or 👎 to help us improve.

⚙️ Customize — Adjust settings in your preferences.

Comment thread src/components/ui/button.tsx Outdated
…ator

Default the indicator by item content instead of a fixed "none": label-only
items (text, no icon) show the dotted-ring→check by default; icon-only and
icon+label items default to "none" so there aren't two icons side by side.
The explicit selectedIndicator prop still overrides either way, so a team can
opt into the ring on icon items if desired.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Move the aria-pressed tint off the base class (where it clobbered filled
variants) onto the outline and ghost variants only. Fixes the AssistantDockControls
regression where the active secondary dock button lost its bg-secondary fill to
the faint --selected tint; primary/secondary/destructive buttons now keep their
own background when aria-pressed. Addresses PR review on button.tsx.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ButtonGroup: command / single-active (aria-pressed) / toggle-buttons /
text+separator / vertical. ToggleGroup: single icon-only vs label-only
(indicator default), multi all-selected, outline+spaced, and sizes. New-story
testCaseIds left "" for the zephyr_sync workflow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…up collapse)

ButtonGroup collapses each non-first button's left border, so the outline
variant's aria-pressed:border-selected-border only recoloured 3 sides of an
active segment — the selection outline looked asymmetric / missing a left
border. Rely on the tint fill + pressed inset instead (matching ToggleGroupItem,
which goes border-0 in a segmented group); segment borders stay uniform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replace the container border (added earlier) with per-item borders + edge
collapse for every variant (not just outline), matching ButtonGroup. The
container border nested a rounded item fill inside a rounded border, so the
selected fill's corners overflowed/misaligned (worst at size=sm, where the
container radius differed from the item radius). With the border and the
rounded corners on the same element, the selected fill always aligns; the
default variant still gets the outline, and adjacent borders collapse into a
single divider. Updated the play test to assert per-item borders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…W-2563

Revert the Toggle/ToggleGroup changes and the shared --selected tokens to main
— those now live in SW-2563 (#196). This PR keeps only the Button aria-pressed
selected state (scoped to outline/ghost) and the ButtonGroup variations story.
Depends on #196 landing first for the --selected tokens.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@boramyi-ts boramyi-ts changed the title feat: SW-2445 unified selected state for toggle & button groups feat: SW-2445 Button/ButtonGroup selected state (aria-pressed) Aug 28, 2026
@boramyi-ts
boramyi-ts force-pushed the SW-2445-selected-state branch from 0d92cec to 3526fad Compare September 1, 2026 16:51
Drop the my-px/mx-px inset so the separator is flush with the buttons instead
of 1px shorter top/bottom, which read as a gap next to an adjacent segment
(e.g. between a button and a ButtonGroupText).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Three related fixes to how a ButtonGroup renders separators and text
labels:

- Drop the trailing border on the element immediately before a
  ButtonGroupSeparator, so the separator is the single divider instead
  of button-border + separator rendering as a doubled line.
- Separator color bg-input -> bg-border, matching the segment borders
  (bg-input was deliberately darker for form fields and stood out).
- ButtonGroupText: remove the grey bg-muted fill; render as quiet
  muted-foreground / font-normal inline text divided by the separator,
  rather than a filled block welded onto the buttons. Matches the look
  MessageBranchPage already hand-rolls for the same "X of Y" pattern.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The aria-pressed treatment on Button referenced bg-selected /
text-selected-foreground / --shadow-inset-pressed, but those tokens only
existed in #196. Tailwind silently emits no CSS for a utility whose theme
key is missing, so merging this before #196 would have shipped a selected
state that renders nothing, with CI green.

Moves the four token lines from #196 into this PR (verbatim, so #196
stays in sync) and adds assertions that fail if they ever go missing.

Also covers the vertical + separator case: the horizontal/vertical
variants strip the preceding child's border when a separator follows, so
the separator is the only thing drawing that divider. The story now
exercises it.

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

Copy link
Copy Markdown
Collaborator

Reviewed this and pushed two fixes to the branch (8db4448). Plain-English rundown of what I found and what I changed.


1. The selected state would have done nothing at all 🔴

The problem, in plain terms: this PR tells buttons to use a colour called selected. But that colour was defined over in #196, not here. So if this PR merged first, the browser would look for "selected", find nothing, and just... skip it. Buttons would look exactly the same as before.

The nasty part: nothing would have told you. Tailwind (our styling tool) doesn't warn about a missing colour — it silently drops the rule. All 10 CI checks would still pass. The Storybook preview would look normal-ish. You'd only find out when someone opened a segmented button group in the real app and asked why nothing highlights.

I confirmed this rather than guessing — compiled the branch's styles and checked whether the rule came out the other end:

Before fix →  EMITTED: NO  (silently dropped)
After fix  →  EMITTED: YES (selected state now real)

What I did: moved the four colour definitions out of #196 and into this PR, copied character-for-character so nothing drifts between the two. This PR now stands on its own — no more "must merge #196 first".

One thing you need to do: #196 still has its own copy of those same four lines, so the two PRs will now collide in src/index.tailwind.css. When that happens, delete them from #196 and keep the ones here. The values are identical, so nothing else in #196 is affected. Not a real conflict, just tidying.


2. Vertical button groups could lose their divider line 🟡

The problem: this PR added a smart rule — when a divider sits between two buttons, remove the button's own border so you don't get a doubled-up 2px line. Good change.

But it assumes the divider is actually drawing a line. In a vertical stack, the divider needs to be told it's horizontal (orientation="horizontal"), otherwise it renders as a 1-pixel-wide vertical sliver — invisible in that layout. So you'd get: button's border removed ✅, divider invisible ❌, and a gap where the line should be.

Nobody would have caught it, because the new Variations story shows a vertical group and a group with a divider, but never both together.

What I did: added a "Vertical with separator" row to the story that uses both at once, plus a test that fails if the divider ever has zero height.


3. The tests weren't really testing the feature 🟡

The original test checked that a button you'd written aria-pressed on... has aria-pressed on it. That is true by definition — it tests that React copies an attribute across, not that the feature works. It would have passed happily through problem #1 above.

What I did: added a test that reads the button's actual rendered colour and checks two things — that it resolves to a real paint (not transparent), and that it differs from an unselected button next to it. That test fails if the colours ever go missing again.


4. Something in this PR changes screens outside its scope 🟡 — needs your eyes

This is the one I can't decide for you.

Alongside the aria-pressed work, this PR also restyles the text label and divider pieces of ButtonGroup. The PR description didn't mention it, so anyone approving "the aria-pressed change" wouldn't realise they were also approving this.

Concretely, the text label loses its grey filled-in background and gets lighter, quieter text — it now reads as a caption rather than a button-shaped chip.

Where that shows up today: the zoom percentage readout in PlateMapEditor (the 100% between the − and + buttons). It'll go from a grey pill to plain text.

The other place it's used (MessageBranchPage, the "1 of 3" in AI messages) already overrides these styles, so it won't change.

I've documented all of this in the PR description rather than reverting it — the change looks deliberate and arguably better. But please open the preview and look at the zoom control before merging, and loop in design if that pill was intentional:

👉 https://ts-lib-ui-kit-storybook-git-sw-2445-select-fd55d1-tetra-science.vercel.app


Verified as fine ✅

  • The bug @unblocked flagged is genuinely fixed. The active dock button keeps its solid fill — I traced through the logic and the new tint can't reach filled buttons any more.
  • The pressed "inset shadow" won't break focus outlines even while its value was missing. I checked this specifically because a missing value there can wipe out a whole shadow rule — Tailwind has a safety net that catches it. Not a problem.

One judgement call for design 🎨

Hovering a selected button now does nothing visually — the selected tint wins over the hover colour. So the active segment gives no hover feedback. That's a defensible choice ("selected sticks"), and I've left it as-is, but it's worth a nod from design since it wasn't an explicit decision.


What I couldn't check

I could not run the interactive Storybook tests locally — this checkout's node_modules is out of date (missing @rdkit/rdkit and @storybook/addon-mcp, both of which are in package.json on main). That's a local environment thing, not something this PR caused. Typecheck and lint both pass clean on the changed files. CI is running the play tests on the pushed commit — please confirm they're green before merging.


TL;DR

Fix 1 was a real "ships broken, tests pass anyway" bug — that one mattered. Fixes 2 and 3 are safety nets. Item 4 just needs you to look at the preview and confirm the zoom readout is meant to change. Then it's good to go.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 96.84% (🎯 83%)
🟰 ±0%
23572 / 24340
🟢 Statements 96.84% (🎯 83%)
🟰 ±0%
23572 / 24340
🟢 Functions 95.08% (🎯 74%)
🟰 ±0%
1084 / 1140
🟢 Branches 90.57% (🎯 81%)
🟰 ±0%
4622 / 5103
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
src/components/ui/button-group.tsx 100%
🟰 ±0%
75%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
src/components/ui/button.tsx 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
Generated in workflow #1152 for commit 8db4448 by the Vitest Coverage Report Action

@54321jenn-ts

Copy link
Copy Markdown
Collaborator

Design review done on the preview — both open visual questions are now closed. Nothing further needed from the author on these.

1. ButtonGroupText restyle — approved. Checked the zoom readout in Design Patterns → Plate Map Editor → Plate zoom buttons. The percentage losing its grey pill fill looks fine. No revert needed.

2. Pressed-in look on selected buttons — reviewed and accepted. The --shadow-inset-pressed inset makes selected segments read as physically depressed rather than merely highlighted. Flagged it because it's a house-style call, not a Button-only one: #196 applies the same token to Toggle and ToggleGroup, so it sets the look for every selected control in the kit. Decision is to keep it as-is. Noting it here so it's a recorded choice rather than an accident, and so #196 doesn't get re-litigated on the same point.

Consistent with that, the hover behaviour also stays: hovering a selected button shows no hover colour, because the selected tint outranks it. Selected sticks.

CI is fully green, including Storybook E2E. The new assertions ran for real in Chromium:

✓ storybook (chromium) src/components/ui/button-group.stories.tsx (3 tests) 763ms
  Test Files  170 passed (170)

That the colour assertion passes is the meaningful bit — it confirms the --selected tint now resolves to an actual paint. Before the token fix, that same assertion would have failed.

Only remaining item is the #196 merge conflict: both PRs now define the same four token lines in src/index.tailwind.css. Drop them from #196, keep these. Values are identical, so nothing else in #196 changes.

54321jenn-ts added a commit that referenced this pull request Sep 3, 2026
…ating

The tokens now live in #195, which needs them to render anything at all.
Keeping a second copy here means whichever PR merges second hits a conflict
on identical lines, so drop ours and consume what #195 lands on main.

This makes the PR depend on #195 merging first. Adds a play-test assertion
on the resolved selected background so that dependency fails loudly: without
the tokens Tailwind emits no css for bg-selected and does not warn, so the
selected state would otherwise render as nothing with CI green.

Expect this branch to be red until #195 is on main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@boramyi-ts boramyi-ts changed the title feat: SW-2445 Button/ButtonGroup selected state (aria-pressed) feat: Button/ButtonGroup selected state (aria-pressed) Sep 3, 2026
@boramyi-ts boramyi-ts changed the title feat: Button/ButtonGroup selected state (aria-pressed) feat: SW-2445 Button/ButtonGroup selected state (aria-pressed) Sep 9, 2026
@boramyi-ts

Copy link
Copy Markdown
Contributor Author

Thanks @54321jenn-ts — verified all four items on the branch (8db4448): the four --selected* token lines are in src/index.tailwind.css (state is no longer a no-op), the "Vertical with separator" story + non-zero-height divider test are in, the color assertion reads a real paint vs an unselected sibling, and the ButtonGroupText restyle is design-approved (no revert).

Fixed the one red check: the PR Title Checker was failing because the title didn't start with the Jira number — retitled to feat: SW-2445 …. All 10 checks now pass.

Two follow-ups, neither on this PR's code:

@54321jenn-ts 54321jenn-ts left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM but merge before #196

@boramyi-ts
boramyi-ts merged commit a77551a into main Sep 10, 2026
14 of 15 checks passed
@boramyi-ts
boramyi-ts deleted the SW-2445-selected-state branch September 10, 2026 15:12
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.

3 participants