Skip to content

feat(icon-button): add IconButton atom (intent×size, isLoading, render prop) — D13#61

Merged
dgraciac merged 1 commit into
mainfrom
feat/icon-button
Apr 30, 2026
Merged

feat(icon-button): add IconButton atom (intent×size, isLoading, render prop) — D13#61
dgraciac merged 1 commit into
mainfrom
feat/icon-button

Conversation

@dgraciac
Copy link
Copy Markdown
Member

Summary

Adds the IconButton atom — icon-only pressable control. Decision D13 (2026-04-30) registers it as a dedicated atom rather than a <Button size="icon"> variant.

State-of-the-art justification: six of eight top-tier DSes (Material 3, MUI, Chakra UI, Radix Themes, IBM Carbon, plus Mantine's ActionIcon) ship a dedicated IconButton. Only shadcn and Polaris fold it into Button. The dedicated atom additionally enforces aria-label (or aria-labelledby) at the type level via a discriminated union — WCAG 4.1.2 compliance moves from runtime warning (Chakra/Mantine pattern) to TypeScript error.

API

<IconButton
  intent="primary | secondary | ghost | destructive"  // mirrors Button
  size="sm | md | lg"                                   // 32 / 40 / 48 px square
  aria-label={string}                                   // required (or aria-labelledby)
  isLoading?={boolean}                                  // swaps icon for <Spinner size={size}/>
  disabled?={boolean}
  type="button | submit | reset"                        // defaults to "button"
  render?={ReactElement}                                // Base UI useRender (same as Button)
>
  <Icon />  {/* Lucide icon, single direct <svg> child */}
</IconButton>

Defaults: intent="ghost" (low-emphasis is the dominant icon-only case — close, dismiss, toolbar action), size="md". border-radius: var(--pharos-radius-full) + square dimensions = perfect circle. Direct-child > svg selector sizes Lucide icons (16 / 20 / 24 px) to match the slot.

Key decisions (full rationale in NAMING-decisions.md)

  • Dedicated atom > <Button size="icon">: six of eight top DSes; type-level a11y; semantic constraints (square ≠ Button rectangle); shadcn's choice optimises for utility-first authoring, which doesn't apply in CSS Modules.
  • Mirror Button's intent axis: same vocabulary across pressables; one-axis instead of Material's four-variant tree.
  • rounded-full (circle): matches Material 3 / MUI / Mantine defaults, plus Alexandria's existing *Circle* wrappers.
  • isLoading on the atom (Button doesn't have it): icon-only loading needs a slot swap, not a sibling spinner; composing two-render-branches at every async call-site is materially worse without it.
  • No integrated Tooltip: diverges from Carbon's mandatory tooltip; aria-label already satisfies WCAG 4.1.2; Tooltip is a separate composition concern (and a separate atom not yet shipped).
  • No selected toggle in v1: no Alexandria call-site exercises the toggle pattern; additive when needed.

Mapping from Alexandria (adoption contract)

Alexandria Pharos
CloseButton <IconButton intent="ghost" aria-label="Close"><X/></IconButton>
CloseButtonCircle <IconButton intent="secondary" aria-label="Close"><X/></IconButton>
Next/PreviousCircleButton <IconButton intent="secondary" aria-label="..."><Chevron/></IconButton>
AsyncLoadingButton (icon-only call-sites) <IconButton isLoading={...} aria-label="..."><Icon/></IconButton>

Adoption ships in a follow-up Alexandria PR after this release lands on npm.

Local quality gates (all green)

  • pnpm build — Vite library + dist-types-smoke ✅
  • pnpm test — 114/114 vitest ✅
  • pnpm typecheck — clean ✅
  • pnpm lint — clean ✅
  • pnpm format:check — clean ✅

Test plan

  • Storybook builds (Chromatic baseline created on first visit).
  • Type check passes in CI.
  • Vitest suite passes in CI (114 tests including 18 new IconButton tests).
  • Lint passes in CI.
  • Format check passes in CI.
  • Changeset present and merges with version-packages PR.
  • Visual review (Chromatic): IconButton stories — Default, Intents, Sizes, Loading, RenderAsLink, Matrix.

🤖 Generated with Claude Code

…r prop) — D13

Dedicated atom rather than `<Button size="icon">` (D13, 2026-04-30): six of
eight top-tier DSes ship a dedicated IconButton (Material 3, MUI, Chakra,
Radix Themes, Carbon, plus Mantine's ActionIcon). The dedicated atom
enforces aria-label (or aria-labelledby) at the type level via a
discriminated union — WCAG 4.1.2 compliance moves from runtime warning to
TypeScript error.

API mirrors Button: intent (primary/secondary/ghost/destructive) × size
(sm/md/lg = 32/40/48 px square) × native button props × Base UI render
prop. Defaults: intent=ghost (the dominant icon-only case is low-emphasis),
size=md. rounded-full + square dimensions = perfect circle. Direct-child
`> svg` selector sizes Lucide icons (16/20/24 px) to match the slot.

isLoading swaps the icon for `<Spinner size={size}/>`, sets disabled, and
exposes aria-busy=true. Wired into the atom (not into Button) because
icon-only loading needs a slot swap, not a sibling spinner.

No integrated Tooltip (diverges from Carbon's mandatory tooltip):
aria-label satisfies WCAG 4.1.2; tooltip is a separate composition concern.
No selected/toggle state in v1 (no Alexandria call-site needs it; additive
when needed).

NAMING-decisions.md documents the full mapping from Alexandria
(CloseButton, CloseButtonCircle, Next/PreviousCircleButton, async-loading
icon buttons) and the deliberate divergences absorbed at adoption time
(aria-label mandatory at TS level, neutral-500 border for secondary, 8px
grid heights).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@chromatic-com
Copy link
Copy Markdown

chromatic-com Bot commented Apr 30, 2026

Tip

All tests passed and all changes approved!

🟢 UI Tests: 7 visual and accessibility changes accepted as baselines
🟢 UI Review: Approved by David Gracia
Storybook icon Storybook Publish: 59 stories published

@dgraciac dgraciac merged commit a3f5ef3 into main Apr 30, 2026
10 checks passed
@dgraciac dgraciac deleted the feat/icon-button branch April 30, 2026 22:17
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