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.