← All proposals

Icon Button

Starting point for an IconButton proposal.

Extends a PDS componentOpen in Storybook

proposals/IconButton/HANDOFF.md74 lines · View on GitHub

Handoff: IconButton (agentic and primary variants, working)

What and why

The agentic treatment for icon-only buttons, matching the Agentic Button. This proposal is the v2 IconButton plus an agentic variant (the AI gradient as the border; it replaced an isAgentic option on 2026-10-21, to be an extension like Send), a primary variant (filled like a primary Button) and isWorking (a spinner). The last two replace the separate SendButton proposal (2026-10-21): Send is a primary icon button. Everything else is unchanged, so the port is a diff against src/components/buttons/IconButton on v2.x.

Exploration and decisions

  • variant='agentic' (2026-10-21). An extension of the Icon Button, like Send: the AI gradient (the same token as the Agentic Button) as the border, always bordered, so hasBorder does not apply. It replaced an isAgentic boolean.
  • Send is a primary icon button (2026-10-21). The SendButton proposal was removed; its three states are made from props: iconName='paperPlane' (send), isWorking (a spinner; disabled; aria-busy) and iconName='stop' (stop, a new filled-square icon in the Icon wrapper). primary is the primary Button's fill and hover and active colors, one 40% fade when disabled (or working), a focus ring offset by 1px. isWorking takes no color option: the spinner is currentColor (SendButton's spinnerColor was dropped; nothing used it).
  • Technique: a transparent border with the gradient on the border-box and the fill on the padding-box, over an opaque surface layer. The fill is a custom property (--icon-button-fill) so hover, active and pressed can swap it. Pressed uses the secondary active background instead of v2's border-color fill, which a gradient cannot provide.

API

Two additions. All other props are unchanged from v2.

Prop Type Default Purpose
isWorking boolean false A spinner replaces the icon; the button is disabled and has aria-busy.
variant adds 'primary' 'standard' agentic is the AI gradient border; primary is filled like a primary Button, for the main action of a toolbar (Send).

Variants and states: every v2 variant, size and state is unchanged. agentic adds the gradient border (rest, hover, active, pressed) at xs, s and m. primary and isWorking add the filled Send button at the same sizes. Stories: Default (agentic), Sizes, With Border, Pressed, Primary All States, Primary Send.

Tokens used

New to this proposal: --pds-gradient-ai-active. The rest is the v2 IconButton's:

  • --pds-animation-transition-button
  • --pds-border-radius-input
  • --pds-border-width-default
  • --pds-border-width-outline
  • --pds-color-button-primary-background-default, -hover, -active, --pds-color-button-primary-foreground, --pds-elevation-button-primary-active
  • --pds-color-button-critical-secondary-*, --pds-color-button-reverse-secondary-*, --pds-color-button-secondary-* (background, border and foreground states)
  • --pds-color-interactive-focus, --pds-color-interactive-reverse-focus
  • --pds-color-surface-default
  • --pds-elevation-button-critical-secondary-active, -reverse-secondary-active, -secondary-active, --pds-elevation-icon-button-pressed
  • --pds-spacing-button-height-m, -s, -xs

Impact

From impact.md: no existing PDS component is reached. The additions are the .pds-icon-button--agentic (variant) rules (rest, hover, active and pressed), which only apply where that class is added. The proposals that build on it are AgenticMenuButton (its icon-only trigger is variant='agentic') and, through it, LabeledDivider (ready for dev). It is also composed by ChatInput (ready for dev). Port IconButton before them (see docs/PORT-ORDER.md). It is also the Send button of ChatInput and the Chat with an agent recipe (variant='primary').

Reused vs net-new

  • Reused: the whole v2 IconButton; Icon and Tooltip. New dependency: the local Spinner (for isWorking) and the stop icon in the local Icon wrapper.
  • Net-new: the isWorking prop, the agentic and primary variants, .pds-icon-button--agentic, --primary and --working, with seven --icon-button-* variables (see custom.md; no raw values).

Accessibility

  • Unchanged from v2: native button, required ariaLabel, tooltip, pressed state, visible focus ring.
  • The gradient border is decorative.
  • The a11y panel has not been run on these stories. Run it in light and dark before the port.

Decisions and open questions

  • Should critical or reverse get the border too?
  • primary has no border option and no agentic treatment. Does Send need the gradient border? Is a waveform (voice) state wanted when the field is empty?
  • The stop icon is drawn in the Icon wrapper (Font Awesome Free solid stop); the port adds it as a registry entry.
  • Strict-null fixes in the local copy (for example icon2Name defaults to undefined, not null) change no behavior. v2 is not strict, this repo is.

Port notes

  • Edit src/components/buttons/IconButton in place. Do not use the generator. isWorking makes IconButton depend on Spinner, so port Spinner first (see docs/PORT-ORDER.md).
  • Swap the local mergeClasses for the real one.

Preview

npm run storybook, then Component Proposals/Buttons/Icon Button.

Figma: https://www.figma.com/design/AMGWJbraJoi2PKWnxo29Ah/PDS-Proposals?node-id=374-48, page Icon Button (4 stories, light only; captures of Storybook with library icons swapped in, sent 2026-10-20).