Components · Actions
Button
Six variants plus an icon-only button, all sharing one height, radius and type treatment — only fill, border and text color change between them.
Preview
Light
Dark
Accessibility
Variants
primary— Brand-filled primary action. One per local action group.secondary— Muted fill for subordinate actions alongside a primary.outline— Bordered secondary — when a border distinction matters in context.ghost— No resting background — toolbars and dense UI.danger— Destructive actions only (delete / irreversible).link— Inline text action; underlined with a 4px offset.icon— Icon-only toolbar button, 32×32.fab— Floating action button — circular, elevated; one per screen.
Spec
The resolved Zetta spec — values and token references straight from the design system. This is the contract your implementation should match.
button-primary
- backgroundColor
- {colors.brand}
- textColor
- {colors.brand-text}
- borderColor
- {colors.brand}
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 500
- lineHeight
- 1.4
- height
- 32px
- paddingX
- 20px
- gap
- 8px
- iconSize
- {icons.sizeMap.sm}
- cursor
- pointer
button-primary-hover
- backgroundColor
- {colors.brand-hover}
- borderColor
- {colors.brand-hover}
button-primary-active
- backgroundColor
- {colors.brand-active}
- borderColor
- {colors.brand-active}
button-primary-focus
- borderColor
- {colors.border-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-secondary
- backgroundColor
- {colors.surface-muted}
- textColor
- {colors.ink}
- borderColor
- transparent
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 500
- lineHeight
- 1.4
- height
- 32px
- paddingX
- 20px
- gap
- 8px
- iconSize
- {icons.sizeMap.sm}
- cursor
- pointer
button-secondary-hover
- backgroundColor
- {colors.surface-pressed}
button-secondary-active
- backgroundColor
- {colors.surface-secondary}
button-secondary-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
button-secondary-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.4
- height
- 32px
- paddingX
- 20px
- gap
- 8px
- iconSize
- {icons.sizeMap.sm}
- cursor
- pointer
button-outline-hover
- backgroundColor
- {colors.surface-muted}
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.4
- height
- 32px
- paddingX
- 16px
- gap
- 8px
- iconSize
- {icons.sizeMap.sm}
- cursor
- pointer
button-ghost-hover
- backgroundColor
- {colors.surface-muted}
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.4
- height
- 32px
- paddingX
- 20px
- gap
- 8px
- iconSize
- {icons.sizeMap.sm}
- 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-link
- backgroundColor
- transparent
- textColor
- {colors.brand}
- borderColor
- transparent
- fontFamily
- Geist
- fontSize
- 14px
- fontWeight
- 500
- lineHeight
- 1.4
- height
- 32px
- paddingX
- 0
- gap
- 8px
- iconSize
- {icons.sizeMap.sm}
- textDecoration
- underline
- textUnderlineOffset
- 4px
- cursor
- pointer
button-link-hover
- opacity
- 0.75
- textDecoration
- none
button-link-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
button-link-disabled
- textColor
- {colors.disabled-text}
- opacity
- 0.5
- textDecoration
- none
- cursor
- not-allowed
- pointerEvents
- none
button-fab
- backgroundColor
- {colors.brand}
- iconColor
- {colors.brand-text}
- borderColor
- {colors.brand}
- borderWidth
- 1px
- borderRadius
- {rounded.full}
- shadow
- {shadows.md}
- cursor
- pointer
- sizeSm
- 32px
- sizeMd
- 40px
- sizeLg
- 48px
- iconSizeSm
- {icons.sizeMap.sm}
- iconSizeMd
- {icons.sizeMap.md}
- iconSizeLg
- {icons.sizeMap.lg}
button-fab-hover
- backgroundColor
- {colors.brand-hover}
- borderColor
- {colors.brand-hover}
- shadow
- {shadows.lg}
button-fab-active
- backgroundColor
- {colors.brand-active}
- borderColor
- {colors.brand-active}
- shadow
- {shadows.md}
button-fab-focus
- borderColor
- {colors.border-focus}
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
button-fab-disabled
- backgroundColor
- {colors.disabled-bg}
- iconColor
- {colors.disabled-text}
- borderColor
- {colors.disabled-border}
- shadow
- none
- cursor
- not-allowed
button-icon
- backgroundColor
- transparent
- iconColor
- {colors.ink}
- borderColor
- transparent
- borderWidth
- 1px
- borderRadius
- {rounded.base}
- width
- 32px
- height
- 32px
- iconSize
- {icons.sizeMap.sm}
- cursor
- pointer
- display
- flex
- alignItems
- center
- justifyContent
- center
button-icon-hover
- backgroundColor
- {colors.surface-muted}
button-icon-active
- backgroundColor
- {colors.surface-pressed}
button-icon-focus
- outline
- 2px solid {colors.border-focus}
- outlineOffset
- 2px
button-icon-disabled
- iconColor
- {colors.disabled-text}
- cursor
- not-allowed
Build with the skill
Zetta is a skill, not a package — there are no classes to import. Hand this prompt to your AI to generate the button in your own project's stack.
Implement the Zetta "Button" component in this project's stack using the zetta-design-md skill.
- Apply the `button` spec from the skill (variants: primary, secondary, outline, ghost, danger, link, icon, fab).
- Use this project's own component conventions and framework idioms.
- Reference the Zetta design tokens (CSS variables) for every color, radius, and shadow — never hardcode values.
- Implement all states (hover, focus, active, disabled, and invalid where applicable) with a visible 2px focus ring.
- Honor the Zetta guardrails (brand never shifts hue, shadows for overlays only, etc.) and verify in Light, Dark, and Accessibility. Anatomy & rules
- All variants: 32px tall, 8px radius, Geist 14 / 500, 8px icon gap, 16px icons.
- Focus shows a 2px ring at 2px offset (danger uses the danger color). Hover changes fill/border only.
- One primary per local action group; reserve
dangerfor irreversible actions.
Accessibility
- Use a real
<button>— keyboard operability and role come for free. Targets are ≥32×32px (44×44 in the Accessibility theme). - The icon-only variant requires an accessible name via
aria-label.
Known gaps
- Loading / submitting state (spinner-in-button) — not yet specified.
- Button group / segmented control, split button, and speed-dial FAB — not yet specified.