Zetta v1.6

Components · Actions

Button

Every button variant in the system. Primary indigo is the CTA; AI purple marks automation and premium; the neutral trio (subtle, outline, ghost) carries supporting actions; danger is the one variant that shifts colour on interaction — darkening in Light, lightening in Dark. 32px tall, 16px paddingX, 8px radius. Note the spec also ships a deprecated brand-lime variant for v0.10 back-compatibility: it is not built here and should not be used, because guardrail 1 reserves brand for identity and never interaction.

Preview

Build with the skill

No package to install — hand this to your AI to generate or migrate the component in your stack.

Prompt for your AI
Apply the Zetta "Button" component to this project using the zetta-design-md skill.

WHEN TO USE
- Migrating or fixing existing code (the common case): audit the current implementation against the `button` spec (variants: primary, ai, primary-tinted, ai-tinted, subtle, outline, ghost, danger, danger-subtle) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `button` spec in this project's stack.

AUDIT & FIX (when the component already exists)
Compare it to the skill's spec and fix, don't just append: hardcoded colors/hex, off-scale radius, wrong fonts, dark/navy fills, shadows used in place of borders (or missing borders), brand lime used as a CTA or interaction (must be primary indigo), and any missing or wrong hover / focus / active / disabled / invalid state or missing 2px focus ring. Replace each with the correct Zetta token.

ALWAYS
- Pull the authoritative spec and tokens from the skill — never guess or hardcode values.
- Use this project's own component conventions and framework idioms.
- Honor the Zetta guardrails (brand = identity only, primary = interaction, shadows for overlays) and verify in Light, Dark, and Accessibility.

Anatomy & rules

┌──────────────────────────────────┐
│  [icon?]  Label text  [icon?]    │  ← 32px height
└──────────────────────────────────┘
  • Container — background + border + radius
  • Label — 14px / 500 Geist, centered
  • Leading icon (optional) — 16px, inherits textColor, 8px gap
  • Trailing icon (optional) — 16px, one per button, not both
  • Focus ring — 2px outline at 2px offset (keyboard only)
  • Loading spinner — replaces label, cursor: wait

Solid — high emphasis, palette-filled

Token Fill Text Focus ring Use
button-primary {colors.primary} Indigo {colors.primary-text} white {colors.border-focus} Primary CTA — one per action group
button-ai {colors.ai} Purple {colors.ai-text} white {colors.ai} AI / premium / discovery actions
button-brand {colors.brand} Lime {colors.brand-text} near-black {colors.brand} Brand moments, marketing CTAs

Tinted — medium emphasis, palette wash

Token Fill Text On hover
button-primary-tinted {colors.primary-bg} {colors.primary} Fills to solid primary
button-ai-tinted {colors.ai-bg} {colors.ai} Fills to solid ai

Tinted buttons communicate palette membership without the full visual weight of solid. Use them as the "default" state for toggleable actions (selected filter, active mode) within a palette-color context.

Neutral — low-to-no emphasis

Token Fill Text Border Use
button-subtle {colors.surface-muted} {colors.ink} none Supporting actions alongside a solid primary
button-outline transparent {colors.ink} {colors.border-strong} Same weight as subtle; use when border separation matters
button-ghost transparent {colors.ink} none Toolbar, utility, low-priority actions

Semantic — destructive

Token Fill Text Use
button-danger {colors.danger-solid} {colors.on-danger} Irreversible actions: delete, remove, purge
button-danger-subtle {colors.danger-bg} {colors.danger} Secondary destructive — "Cancel plan", less permanent than delete
State Background Border Text Notes
Default Variant fill (see variant matrix) Variant border Variant text —
Hover Variant hover fill Variant border Variant text Subtle brightness shift
Focus Variant fill 2px solid {colors.border-focus} Variant text Keyboard only
Active {colors.surface-pressed} overlay Variant border Variant text Mouse down
Selected {colors.primary-bg} {colors.primary} {colors.primary} Toggle-style buttons
Disabled {colors.disabled-bg} {colors.disabled-border} {colors.disabled-text} not-allowed cursor
Loading Variant fill Variant border Hidden — spinner shown aria-busy="true"

When to use which solid variant

  • Primary (Indigo) — the top action in any group: "Save", "Submit", "Create". One per group.
  • AI (Purple) — AI-generated or discovery actions: "Generate", "Suggest", "Summarize".
  • Brand (Lime) — reserved for brand moments: marketing landing pages, onboarding hero CTAs. Rare in product UI.

When to use tinted vs solid

Use tinted when:

  • The action is a default/unselected state that can be toggled (e.g. a filter chip).
  • You want palette membership without full visual dominance (e.g. alongside a solid primary).
  • The surface is already colored and a solid fill would create too much contrast.

Use solid when:

  • The action is the single most important action on screen.
  • Maximum contrast against a neutral surface is required.

When to use neutral variants

  • Subtle — most common supporting action. "Cancel", "Back", "View details".
  • Outline — same hierarchy as subtle; prefer when the button is placed on a surface where the border provides useful visual separation.
  • Ghost — toolbar actions, icon-adjacent controls, any place where visual weight must be nearly zero.

Do

  • One solid primary per action group.
  • 8px minimum gap between adjacent buttons.
  • Primary action rightmost in a horizontal group.
  • Match loading label to the in-progress verb: "Saving…" not "Loading…".

Don't

  • Don't put two solid palette buttons side by side — use solid + subtle or solid + outline.
  • Don't use button-brand for product UI actions — it's a marketing color.
  • Don't use button-danger without a confirmation step (dialog or inline confirmation).
  • Don't use ghost for the primary action.
  • Verbs or short verb phrases: "Save", "Delete project", "Send report".
  • Sentence case. 1–4 words maximum.
  • Danger buttons: be specific — "Delete project", not "Delete".
  • Loading: "Saving…", "Uploading…", "Generating…" — match the action verb.

Keyboard

Key Behavior
Tab Move focus to the button
Shift + Tab Move focus to the previous element
Enter / Space Activate the button action
Space Toggle pressed state for toggle-style buttons

Accessibility

  • All variant label + background pairs meet 4.5:1 contrast (AA).
  • Focus ring: 2px outline at 2px offset. Danger variants use {colors.danger} outline.
  • Loading: aria-busy="true" on the button; spinner has visually hidden "Loading…" label.
  • Disabled: disabled attribute removes from focus order. Use aria-disabled="true" only when the button must remain reachable for explanation.

Spec

Values and token references resolved straight from the Zetta spec.

button-primary
backgroundColor
{colors.primary}
textColor
{colors.primary-text}
borderColor
{colors.primary}
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.40
height
32px
paddingX
{spacing.md}
gap
{spacing.sm}
iconSize
{icons.sizeMap.md}
cursor
pointer
button-primary-hover
backgroundColor
{colors.primary-hover}
borderColor
{colors.primary-hover}
button-primary-active
backgroundColor
{colors.primary-active}
borderColor
{colors.primary-active}
button-primary-focus
outline
2px solid {colors.border-focus}
outlineOffset
2px
button-primary-disabled
backgroundColor
{colors.disabled-bg}
textColor
{colors.disabled-text}
borderColor
{colors.disabled-border}
cursor
not-allowed
button-ai
backgroundColor
{colors.ai}
textColor
{colors.ai-text}
borderColor
{colors.ai}
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.40
height
32px
paddingX
{spacing.md}
gap
{spacing.sm}
iconSize
{icons.sizeMap.md}
cursor
pointer
button-ai-hover
backgroundColor
{colors.ai-hover}
borderColor
{colors.ai-hover}
button-ai-active
backgroundColor
{colors.ai-active}
borderColor
{colors.ai-active}
button-ai-focus
outline
2px solid {colors.ai}
outlineOffset
2px
button-ai-disabled
backgroundColor
{colors.disabled-bg}
textColor
{colors.disabled-text}
borderColor
{colors.disabled-border}
cursor
not-allowed
button-brand
backgroundColor
{colors.brand}
textColor
{colors.brand-text}
borderColor
{colors.brand}
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.40
height
32px
paddingX
{spacing.md}
gap
{spacing.sm}
iconSize
{icons.sizeMap.md}
cursor
pointer
button-brand-hover
backgroundColor
{colors.brand-hover}
borderColor
{colors.brand-hover}
button-brand-active
backgroundColor
{colors.brand-active}
borderColor
{colors.brand-active}
button-brand-focus
outline
2px solid {colors.ink}
outlineOffset
2px
button-brand-disabled
backgroundColor
{colors.disabled-bg}
textColor
{colors.disabled-text}
borderColor
{colors.disabled-border}
cursor
not-allowed
button-primary-tinted
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
borderColor
transparent
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.40
height
32px
paddingX
{spacing.md}
gap
{spacing.sm}
iconSize
{icons.sizeMap.md}
cursor
pointer
button-primary-tinted-hover
backgroundColor
{colors.primary}
textColor
{colors.primary-text}
button-primary-tinted-focus
outline
2px solid {colors.border-focus}
outlineOffset
2px
button-primary-tinted-disabled
backgroundColor
{colors.disabled-bg}
textColor
{colors.disabled-text}
cursor
not-allowed
button-ai-tinted
backgroundColor
{colors.ai-bg}
textColor
{colors.ai}
borderColor
transparent
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.40
height
32px
paddingX
{spacing.md}
gap
{spacing.sm}
iconSize
{icons.sizeMap.md}
cursor
pointer
button-ai-tinted-hover
backgroundColor
{colors.ai}
textColor
{colors.ai-text}
button-ai-tinted-focus
outline
2px solid {colors.ai}
outlineOffset
2px
button-ai-tinted-disabled
backgroundColor
{colors.disabled-bg}
textColor
{colors.disabled-text}
cursor
not-allowed
button-subtle
backgroundColor
{colors.surface-muted}
textColor
{colors.ink}
borderColor
transparent
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.40
height
32px
paddingX
{spacing.md}
gap
{spacing.sm}
iconSize
{icons.sizeMap.md}
cursor
pointer
button-subtle-hover
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
button-subtle-active
backgroundColor
{colors.surface-secondary}
button-subtle-focus
outline
2px solid {colors.border-focus}
outlineOffset
2px
button-subtle-disabled
backgroundColor
{colors.disabled-bg}
textColor
{colors.disabled-text}
cursor
not-allowed
button-outline
backgroundColor
transparent
textColor
{colors.ink}
borderColor
{colors.border-strong}
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.40
height
32px
paddingX
{spacing.md}
gap
{spacing.sm}
iconSize
{icons.sizeMap.md}
cursor
pointer
button-outline-hover
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
button-outline-active
backgroundColor
{colors.surface-pressed}
button-outline-focus
borderColor
{colors.border-focus}
outline
2px solid {colors.border-focus}
outlineOffset
2px
button-outline-disabled
textColor
{colors.disabled-text}
borderColor
{colors.disabled-border}
cursor
not-allowed
button-ghost
backgroundColor
transparent
textColor
{colors.ink}
borderColor
transparent
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.40
height
32px
paddingX
{spacing.md}
gap
{spacing.sm}
iconSize
{icons.sizeMap.md}
cursor
pointer
button-ghost-hover
backgroundColor
{colors.primary-bg}
textColor
{colors.primary}
button-ghost-active
backgroundColor
{colors.surface-pressed}
button-ghost-focus
outline
2px solid {colors.border-focus}
outlineOffset
2px
button-ghost-disabled
textColor
{colors.disabled-text}
cursor
not-allowed
button-danger
backgroundColor
{colors.danger-solid}
textColor
{colors.on-danger}
borderColor
{colors.danger-solid}
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.40
height
32px
paddingX
{spacing.md}
gap
{spacing.sm}
iconSize
{icons.sizeMap.md}
cursor
pointer
button-danger-hover
backgroundColor
{colors.danger-solid-hover}
borderColor
{colors.danger-solid-hover}
button-danger-active
backgroundColor
{colors.danger-solid-active}
borderColor
{colors.danger-solid-active}
button-danger-focus
outline
2px solid {colors.danger}
outlineOffset
2px
button-danger-disabled
backgroundColor
{colors.disabled-bg}
textColor
{colors.disabled-text}
borderColor
{colors.disabled-border}
cursor
not-allowed
button-danger-subtle
backgroundColor
{colors.danger-bg}
textColor
{colors.danger}
borderColor
transparent
borderWidth
1px
borderRadius
{rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.40
height
32px
paddingX
{spacing.md}
gap
{spacing.sm}
iconSize
{icons.sizeMap.md}
cursor
pointer
button-danger-subtle-hover
backgroundColor
{colors.danger-solid}
textColor
{colors.on-danger}
button-danger-subtle-focus
outline
2px solid {colors.danger}
outlineOffset
2px
button-danger-subtle-disabled
backgroundColor
{colors.disabled-bg}
textColor
{colors.disabled-text}
cursor
not-allowed
button-loading
cursor
wait
pointerEvents
none
opacity
0.75
button-spinner
width
14px
height
14px
borderWidth
1.5px
borderStyle
solid
borderColor
currentColor
borderTopColor
transparent
borderRadius
{rounded.full}
animation
spin {motion.duration-slow} linear infinite

Known gaps

  • FAB speed-dial (multiple floating actions) — not yet specified.