Skip to main content

Button Icon-Only interaction states (DES-87)

Version: 0.2.0 · Type: ✨ Feature

2026-06-16 DES-87 Button Icon-Only interaction states (Tertiary / Quaternary)

Problem

The Figma Design System adjusted the background tokens for the Button Icon-Only Tertiary / Quaternary interaction states, but in the frontend implementation the interaction states of tertiary / quaternary shared one set of styles across all sizes, with no distinction for Icon-Only. As a result, the Hover / Clicked feedback of Icon-Only buttons was inconsistent with the new Figma:

  • Tertiary Icon-Only Hover still used the Gray-series background (should be the Tinted series)
  • Quaternary Icon-Only Hover / Clicked still overlaid a Tinted background (should remove the background and only change the icon color)

Changed Files

  • packages/design/src/components/Button/styles.ts
  • packages/design/src/components/Button/Button.test.tsx
  • packages/design-site/docs/components/atomic/button.mdx

Changes

  • Added BUTTON_ICON_ONLY_VARIANT_CLASS: provides dedicated interaction styles for tertiary / quaternary only when size === 'icon'; text buttons (other sizes) behave exactly as before
    • Tertiary Icon-Only: Hover Grays/Tinted/Default, Clicked Grays/Tinted/Emphasized (replacing the original Gray-1 / Gray-2)
    • Quaternary Icon-Only: removes the hover/active background and switches to icon color changes — Default Labels/Tertiary → Hover Labels/Primary → Clicked Labels/Secondary
  • When size === 'icon', getButtonTokenClass prefers the Icon-Only variant class, falling back to the generic variant class on a miss
  • Added unit tests: cover the new state classes for Tertiary / Quaternary Icon-Only, and assert that text-size buttons keep their original background
  • design-site Button docs add an Icon-Only interaction state description in the Icon Button section

Notes

(Figma nodes)

  • Tertiary Hover: 20935:1573
  • Quaternary Hover: 21220:2584
  • Quaternary Clicked: 21220:2586

(Linear)


2026-06-16 Button disabled / loading state restores the not-allowed cursor

Problem

Every variant of BUTTON_DISABLED_CLASS already declared disabled:cursor-not-allowed, but it had no effect: the UI structure layer BUTTON_STRUCTURE_CLASS always carries disabled:pointer-events-none (unstyledVisual only turns off visuals and keeps the structure classes), and pointer-events: none disables cursor styling. The result was that disabled buttons, as well as loading buttons (loading sets the disabled attribute), showed only the default cursor instead of not-allowed.

Changed Files

  • packages/design/src/components/Button/styles.ts
  • packages/design/src/components/Button/Button.test.tsx

Changes

  • BUTTON_BASE_CLASS adds disabled:pointer-events-auto, which via mergeClass (tailwind-merge) overrides the structure layer's disabled:pointer-events-none, making the existing disabled:cursor-not-allowed actually take effect; clicks are still intercepted by the native disabled attribute
  • Disabled visuals are unaffected: in Tailwind, the disabled: variant is generated after hover: / active:, so at the same specificity disabled still overrides hover/active colors
  • loading and disabled share the disabled attribute, so a single fix covers both states
  • Knock-on fix for the link variant: after disabled:pointer-events-auto re-enables pointer events, hovering a link triggers hover:underline, causing an underline to appear on hover in the disabled/loading state. Added disabled:no-underline to the disabled classes of link-color / link-gray (the disabled: variant is generated after hover:, overriding hover:underline)
  • Added unit tests: the loading state asserts disabled:pointer-events-auto + disabled:cursor-not-allowed; the primary disabled case adds the same two assertions; the link variant disabled/loading asserts disabled:no-underline
  • Audited the remaining interactive components (Input / Textarea / Select / Checkbox / RadioGroup / Switch / Upload / DatePicker / TimePicker / Calendar / Tree / Tabs / DropdownMenu / AiButton, etc.): found no actual instance of the same "cursor-not-allowed canceled out by pointer-events-none" issue (most use unstyledVisual to remove the UI-layer pointer-events-none, or use Radix data-disabled with a cursor element that has no pointer-events-none); Button is the only existing instance