# Desktop UI Buttons Controls

> Use whenever designing, building, or reviewing buttons, icon buttons, toggles, switches, checkboxes, radio buttons, segmented controls, or any other clickable/pressable control in a desktop app. Depends on desktop-ui-foundations for tokens — read that first if it hasn't been read this session. Trigger on "design a button," "this button looks off," "add a toggle/switch," "button states," or any control/interaction-hierarchy question.

- Skill: `simply-ehis/desktop-ui-buttons-controls` (Agent Skill)
- Install (CLI): `npx skillmds@latest add simply-ehis/desktop-ui-buttons-controls`
- Raw SKILL.md: https://api.skillmd.com/api/skills/simply-ehis/desktop-ui-buttons-controls/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: simply-ehis (https://skillmd.com/u/simply-ehis)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/simply-ehis/desktop-ui-buttons-controls

---


# Buttons & Controls (Desktop)

## Hierarchy first, styling second

Before styling a single button, decide how many hierarchy levels this screen actually needs. Most screens need exactly three:

| Level | Use for | Visual weight |
|---|---|---|
| Primary | The one action you want taken | Filled, brand color, highest contrast |
| Secondary | Alternative or supporting actions | Outlined or tonal (soft-filled), medium contrast |
| Tertiary / ghost | Low-stakes or frequent actions (toolbar icons, "cancel") | Text-only or icon-only, no border, lowest contrast |

**One primary button per view, maximum.** If a screen wants two visually loud actions, that's a sign the screen is doing two jobs — split it, or demote one to secondary. This is the single fastest fix for the "uniform button weight" AI tell.

**Destructive actions get their own color**, not just a secondary style — a red/danger accent, always with a confirmation step for anything irreversible (see `desktop-ui-surfaces-overlays` for confirmation dialogs).

## Sizing

Use exactly 3 sizes, tied to the spacing scale from foundations:

- `sm` — 28–32px tall, dense toolbars, inline table actions
- `md` — 36–40px tall, the default for almost everything
- `lg` — 44–48px tall, rare — a single hero action, not a whole screen of them

Horizontal padding should be roughly `1.5×` the control's font size on each side. Icon-only buttons should be square (equal width/height) at the same three sizes, with the icon centered and sized to about 60% of the control's height.

**Minimum hit target: 24×24px even if the visible control is smaller.** Toolbar icon buttons especially — pad the clickable area beyond the drawn icon rather than shrinking the target to match tight visual spacing.

## States — every control needs all of these, not just default and hover

| State | What changes | Notes |
|---|---|---|
| Default (rest) | Base style | — |
| Hover | Subtle background/border shift, cursor becomes pointer | Gate behind `(hover: hover) and (pointer: fine)` so touch/trackpad taps don't get stuck in a fake hover state |
| Active / pressed | `transform: scale(0.97)` plus a slightly darker/tonal shift, ~120–160ms ease-out | This is the single highest-leverage "feels responsive" cue in the whole app — see `desktop-ui-motion` |
| Focus (keyboard) | Visible focus ring, distinct from hover | Never remove focus rings without replacing them — desktop power users navigate by keyboard constantly |
| Disabled | Reduced opacity (~40–50%) AND `cursor: not-allowed`; never rely on opacity alone since it reads as "loading" as easily as "disabled" | Disabled controls should still be legible enough to read their label |
| Loading | Spinner or progress replaces the label/icon, control stays the same size (no layout shift), stays disabled | Never let a button resize itself between idle and loading — reserve the space |

## Toggles, checkboxes, radio buttons, segmented controls

- **Checkbox**: multi-select within a set, independent options.
- **Radio**: single-select within a visible set of 2–6 options where all options should be visible at once.
- **Switch/toggle**: a single on/off setting that takes effect immediately (not "submit to apply"). Never use a toggle for something that requires a save action — that's a checkbox or a select.
- **Segmented control**: single-select where seeing all options side-by-side, and switching between them instantly, matters more than saving space (e.g. view mode: List / Grid / Timeline).

All four need the same state coverage as buttons above, plus a clear, animated (not instant-snap) transition for the checked/unchecked state — a switch thumb should slide, not teleport (120–180ms, see `desktop-ui-motion`).

## Icon buttons specifically

- Never ship an icon-only button without a tooltip (see `desktop-ui-text-inputs`/`desktop-ui-feedback-messaging` for tooltip timing) unless the icon is truly universal (close ×, and even that benefits from one).
- Keep icon buttons visually consistent with the icon rules in `desktop-ui-icons` — same stroke weight, same optical size, across the whole toolbar.
- A row of icon buttons should have even spacing at the token scale (usually 4 or 8px gaps), not eyeballed.

## Review format

When reviewing existing button/control code, use this table, one row per issue:

| Before | After | Why |
|---|---|---|
| `transition: all 0.3s` on a button | `transition: transform 150ms ease-out, background-color 150ms ease-out` | Animating `all` is imprecise and can catch properties you didn't intend to transition |
| No `:active` state | `transform: scale(0.97)` on press | Missing press feedback is the fastest way for a control to feel dead |
| Disabled button at full color, just `pointer-events: none` | Reduced opacity + `cursor: not-allowed` + still `pointer-events: none` | Disabled must be visually obvious, not just functionally inert |
| Three buttons, all filled brand color, same size | One filled (primary), two outlined/ghost (secondary/tertiary) | Establishes which action the user should actually take |

