proposals/IconButton/decisions.md25 lines · View on GitHub
Decisions
Base is the unmodified v2 IconButton (IconButton.tsx, icon-button.css, stories). Proposal changes go on top.
Icon, Tooltip and types come from the published package; mergeClasses is a local stand-in (the port uses the real one).
Strict-null fixes only, behavior unchanged: icon2Name defaults to undefined instead of null, refs typed HTMLDivElement, title uses undefined instead of null. v2 isn't strict, this repo is.
Storybook: both Button proposals sit under Proposals/Buttons/ (Button, Icon Button).
isAgentic boolean modifier, same model as Button (not a variant or a separate component). AI gradient (--pds-gradient-ai-active) as the border, same token as Button, the agentic Spinner and pds-ai-active-border.
Only the standard variant supports it (the secondary-style one); reverse and critical warn in dev. Open: should critical or reverse get a border too?
isAgentic recolors the border; it needs hasBorder. Agentic + border = AI gradient border; border alone = plain border; neither = borderless icon. Without hasBorder, isAgentic changes nothing (an earlier version drew the border on its own, which read as a second kind of border).
Implementation: transparent border + gradient on the border-box, fill layers on the padding-box, opaque surface layer under translucent fills. The fill is a custom property (--icon-button-fill) so hover, active and pressed swap it. Pressed uses the secondary active background instead of v2's border-color fill, which the gradient can't provide.
Storybook defaults hasBorder and isAgentic to true (handoff). Sizes shows a borderless row and a bordered row; the bordered row follows the isAgentic control (on = AI gradient border, off = default gray).
Primary variant and working state: Send is an icon button (2026-10-21)
Decision:variant='primary' (filled like a primary Button) and isWorking (a spinner replaces the icon; disabled; aria-busy) were added, and the SendButton proposal was removed. Send's three states come from props: iconName='paperPlane', isWorking, iconName='stop'. The stop icon (a filled square) was added to the Icon wrapper. Disabled, including working, is one 40% fade of the whole button, as the SendButton had it. isWorking has no color prop; the spinner is currentColor (the SendButton's spinnerColor was unused).
Why: the designer decided the Send button should be a variant of the icon button with the primary fill, not a separate component.
Port impact: one component fewer to port. IconButton now imports the Spinner proposal, so Spinner is ported first. The Icon wrapper gains stop.
Open:isAgentic is not offered on primary; does Send need it? A voice (waveform) state for an empty field was never built.
Agentic is a variant, not a boolean (2026-10-21)
Decision:isAgentic was replaced by variant='agentic', the same way Send became variant='primary': an extension of the existing Icon Button, not a separate component (the Agentic Button, Agentic Menu Button and Agentic Split Button are separate). The agentic variant always has the gradient border, so hasBorder does not apply to it, and the "needs hasBorder" rule is gone. The Menu Button's icon-only trigger moved to the Agentic Menu Button, which uses variant='agentic'.
Why: the designer decided the agentic icon button should be an extension of the Icon Button, like Send.
Port impact:isAgentic is not ported; add the agentic value to variant. The CSS is the same gradient-border technique.