Skip to content

Add CartButton, a toggle that puts an item in the cart - #48

Merged
dauglyon merged 2 commits into
mainfrom
cart-button
Sep 10, 2026
Merged

Add CartButton, a toggle that puts an item in the cart#48
dauglyon merged 2 commits into
mainfrom
cart-button

Conversation

@dauglyon

@dauglyon dauglyon commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

What it is

A 30px pill wearing Button's outline variant, level with NavIcon and the sm buttons, holding PlusCircle at rest and CheckCircle once the item is in. It composes Button's classes rather than wrapping the component, the way Field composes Input, so a nested render does not run useButton twice.

  • State: Base UI's Toggle. pressed / defaultPressed / onPressedChange, aria-pressed, keyboard. The icon form keeps one name, "Add to cart", and aria-pressed carries the state; the labelled form is named by its visible words, which change with the state.
  • Colour: primary ink (--ct-primary) and the primary tint border (--bo-primary) while the action is open; neutral ink (--c-ink3) and border (--c-border2) once the item is in. The ground is outline's: transparent at rest, raised on hover. --ct-primary clears WCAG AA on every ground in both themes.
  • Words on hover or focus: a Base UI Tooltip whose positioner is offset by the pill's own width (−30), so the popup is the pill drawn again over the trigger and extended to the right. Nothing in flow changes width. Base UI flips it left when there is no room; [data-side='left'] reverses the order so the glyph stays over the trigger. The popup is aria-hidden and forwards its click to the trigger; data-instant="focus" redraws the focus ring on it, since it covers the trigger's.
  • labelled: words stay in the pill for a prominent placement; the tooltip is disabled.

Two Button rules the pill has to override

  • .btn svg { display: block } outranks a bare .check { display: none }, so the unpressed pill showed both glyphs. The glyph rules anchor to .pill.
  • .btn[data-pressed] { transform: scale(0.97) } is Button's press animation. On a Toggle, data-pressed is the on state, so the in-cart pill sat at 97%. The pill resets the transform. No Button emits data-pressed, so that half of Button's selector could go in a follow-up.

Where it lands

Consumer How
React import { CartButton } from '@kbase/design-system'
Showcase Actions, after Copy button
CSS-only (components.css) kb-button--btn kb-button--outline kb-cart-button--pill on a <button>, data-pressed for the state, data-labelled for the words. kb-button--* precede it in the sheet and in the bundle, so the pill's rules win at equal specificity. The popup is React-only.

Checks

typecheck, lint, vitest (189 passed), build:design-system, gen_portal_css.py with the repo's Sass — all green locally at 9cf7207. The Python wheel job was not run here.

One note for review

The mirror of pressed in useState exists only because the popup is portaled and cannot read the trigger's data-pressed. The Toggle is still the source of truth for the trigger; the mirror feeds the popup's glyph, words, and its own data-pressed.

KBase design system and others added 2 commits September 9, 2026 14:43
A 30px pill wearing Button's outline variant, level with NavIcon and the sm buttons, holding PlusCircle at rest and CheckCircle once the item is in. Base UI's Toggle owns the pressed state, controlled through `pressed` or not through `defaultPressed`, and its aria-pressed and keyboard handling; the name stays "Add to cart" and the state says whether it is in.

On hover or focus the words appear beside the plus in a Base UI Tooltip whose positioner is offset by the pill's own width, so the popup is the pill drawn again over the trigger and extended to the right. Nothing in the row changes width. Base UI flips it to the left when there is no room, and `data-side` reverses the order so the glyph stays over the trigger. The popup is aria-hidden and forwards its click to the trigger. `labelled` keeps the words in the pill for a prominent placement and disables the tooltip.

Exported from the package, shown in Actions, and in components.css as kb-cart-button--pill on top of kb-button--btn and kb-button--outline, which precede it in the sheet and in the bundle, so the pill's radius wins at equal specificity. State is Base UI's data-pressed, per the class-name rule.
Button's `.btn svg { display: block }` outranked the bare `.check` rule, so
the unpressed pill showed both glyphs; the glyph rules now anchor to `.pill`.
Button's `[data-pressed]` press animation is the Toggle's on state here, so
the in-cart pill stayed at 97%; the pill resets the transform.

Colours: primary ink and the primary tint border while the action is open,
neutral ink and border once the item is in. The popup takes `data-pressed`
from React to follow, since it is portaled.

Comments, prop docs, and the showcase paragraphs reworded. The claim that
the name never changes held only for the icon form, and the forwardRef
reason given was popup positioning, which the Trigger's own ref handles;
Button's render-element reason is the one that applies.
@dauglyon
dauglyon merged commit 6f30fe4 into main Sep 10, 2026
6 checks passed
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.

1 participant