Skip to content

Button axes: style families, hover/press effects, group layout #613

Description

@mehdibha

Research-backed axis model for buttons in the builder, from an Aug 2026 survey of 16 design systems (CSS verified from source, not docs). Prototyped in panel-lab v2 (see linked PR); this issue tracks the product decisions and the eventual registry implementation.

The problem

The old panel-lab Buttons section's "Style" row was the variant enum (primary/secondary/quiet…) relabeled. Variants are semantic roles shipped in the end user's API — not a design-system aesthetic. Input already models this correctly (params.style in input/meta.ts); Button has no style param at all.

Findings

  • No builder separates style from variant for buttons. The industry crams aesthetics into variant enums (Radix classic, Mantine gradient, M3 elevated, HeroUI v2 shadow) or hides them in whole-theme presets. Radix's classic shows why that fails: it exists only for the solid look — no classic-outline/soft/ghost — so a whole-app 3D switch is impossible. A per-component style axis is a differentiator.
  • Styles reshape only fill-bearing variants; quiet/link are invariant. Radix, Untitled UI, Primer, and Geist all independently converged on this.
  • Hover: 0/16 systems use "none", 0/16 use lift (Stripe removed its famous lift). 14/16 dim (token swap, alpha, or Material state layer); Linear and Ant lighten.
  • Press is where systems diverge: darker -active token (8/16), nothing (5/16), scale .97 (Linear, HeroUI, Spectrum 2 pressScale), translate-y 1px (shadcn v4 styles). The state is named active (10) or pressed (5, incl. React Aria). Press stays uniform across variants (Linear precedent); hover mechanics vary per variant everywhere (ghost gains a bg fill on hover in every system).
  • Button groups: three axes cover all 14 systems surveyedgroupLayout (attached / gapped / container-track), separator (border / divider / none), and selectedStyle for the toggle flavor (fill / chip / inverse / M3 check-fill / M3 pill-morph). Today's ToggleButtonGroup hardcodes exactly one combination (attached, square inner corners). Most systems split action groups from segmented controls — maps to the Button ⇄ ToggleButton synced group; container belongs to the toggle flavor.

Proposed axes

  • style: flat (default; Geist/Chakra) · outline (Primer hairline border + resting shadow) · raised (3D: Radix classic / Untitled UI — gradient + inset ring + rim light + bottom shade) · elevated (Stripe/M3 soft layered shadow). Hover/press mechanics resolve per style family; styles need per-state slots and a dark-mode token swap (raised).
  • hover: dim (default) · lighten (Linear) · none
  • press: dim (default) · scale (.97) · push (1px) · none
  • Group: groupLayout · separator · selectedStyle (toggle only)
  • Advanced tier: transition duration (observed band 150–200ms), separator inset, pressed-vs-selected distinction (Polaris separates them).

Open questions

  • Naming: "outline" as a style collides with the outline variant reading — "hairline"/"bordered" may be clearer.
  • Whether/when this becomes a real params.style on button/meta.ts — registry + publisher work, touching the deferred variant-ladder follow-up (feat(buttons): rename default variant to secondary #531).
  • M3 pill-morph and ripple are deliberately out of scope (single-system outliers).

Full survey tables with per-system sources (Radix base-button.css, Primer ButtonBase.module.css, Linear's shipped CSS, shadcn style-nova.css, HeroUI theme, Spectrum pressScale.ts, Carbon/Cloudscape/Ant/Chakra/Mantine/Polaris/Atlassian/M3/Geist/Stripe) live in the research session; ask if needed.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions