feat(icon-button): add IconButton atom (intent×size, isLoading, render prop) — D13#61
Merged
Conversation
…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>
|
Tip All tests passed and all changes approved!🟢 UI Tests: 7 visual and accessibility changes accepted as baselines |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the
IconButtonatom — 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 dedicatedIconButton. Only shadcn and Polaris fold it into Button. The dedicated atom additionally enforcesaria-label(oraria-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
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> svgselector sizes Lucide icons (16 / 20 / 24 px) to match the slot.Key decisions (full rationale in
NAMING-decisions.md)<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.intentaxis: 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.isLoadingon 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.aria-labelalready satisfies WCAG 4.1.2; Tooltip is a separate composition concern (and a separate atom not yet shipped).selectedtoggle in v1: no Alexandria call-site exercises the toggle pattern; additive when needed.Mapping from Alexandria (adoption contract)
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
🤖 Generated with Claude Code