Zetta v1.6

Components · Actions

Split button

A primary action with a built-in menu of alternatives — "Save" beside a caret that opens "Save as", "Save and close". Both halves share the Primary fill, joined by a faint divider so the default action stays one tap away while variants hide behind the trigger.

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 "Split 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-split` spec (variants: action, trigger, disabled) and CORRECT every deviation — replace off-spec values, don't only add new ones.
- New component: build it to the `button-split` 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

┌─────────────────────┬────┐
│   Primary Action    │ ▾  │
└─────────────────────┴────┘
         ↑               ↑
  split-button-action   split-button-trigger

Split Button has two interactive zones — the main action and the dropdown trigger.

State Main action Dropdown trigger
Default Primary fill Primary fill, divider border
Hover (main) {colors.primary-hover} Unchanged
Hover (trigger) Unchanged {colors.primary-hover}
Focus (main) 2px solid {colors.border-focus} outline Unchanged
Focus (trigger) Unchanged 2px solid {colors.border-focus} outline
Active {colors.primary-active} on pressed zone
Disabled {colors.disabled-bg} on both zones {colors.disabled-text} on both
Loading Spinner on main zone Trigger hidden

When to use

  • When a primary action has 2–4 closely related variants (e.g. "Save" with "Save and close", "Save as draft").

When not to use

  • When the secondary options are unrelated to the primary action — use a separate button and dropdown.
  • When there is only one action — use a standard button-primary.

Do

  • Keep the primary action as the most common choice.
  • Limit dropdown items to 2–4 closely related variants.

Don't

  • Don't use for destructive actions — the accidental activation risk is too high.
  • Don't nest a split button inside a toolbar with other split buttons.
  • Main action label: verb, 1–3 words. "Save", "Export", "Send report".
  • Dropdown trigger: aria-label="More [action] options" — e.g. "More save options".
  • Dropdown items: related variants of the main action only. "Save as draft", "Save and publish".

Keyboard

Key Behavior
Tab Move focus to the main action button
Tab (again) Move focus to the dropdown trigger
Enter / Space Activate the focused zone (main action or open dropdown)
Down Arrow Open the dropdown panel when trigger is focused
Escape Close the dropdown panel

Accessibility

  • Two separate <button> elements — do not merge into one.
  • Trigger button: aria-haspopup="menu", aria-expanded reflects open state, aria-label="More options".

Spec

Values and token references resolved straight from the Zetta spec.

split-button-container
display
flex
alignItems
stretch
gap
0
split-button-action
backgroundColor
{colors.primary}
textColor
{colors.primary-text}
borderColor
{colors.primary}
borderWidth
1px
borderRadius
{rounded.base} 0 0 {rounded.base}
fontFamily
Geist
fontSize
14px
fontWeight
500
lineHeight
1.4
height
32px
paddingX
{spacing.md-lg}
cursor
pointer
split-button-action-hover
backgroundColor
{colors.primary-hover}
borderColor
{colors.primary-hover}
split-button-action-active
backgroundColor
{colors.primary-active}
borderColor
{colors.primary-active}
split-button-action-focus
outline
2px solid {colors.border-focus}
outlineOffset
2px
split-button-divider
width
1px
backgroundColor
{colors.primary-text}
opacity
0.3
alignSelf
stretch
split-button-trigger
backgroundColor
{colors.primary}
iconColor
{colors.primary-text}
borderColor
{colors.primary}
borderWidth
1px
borderRadius
0 {rounded.base} {rounded.base} 0
width
32px
height
32px
iconSize
{icons.sizeMap.md}
cursor
pointer
display
flex
alignItems
center
justifyContent
center
split-button-trigger-hover
backgroundColor
{colors.primary-hover}
borderColor
{colors.primary-hover}
split-button-trigger-active
backgroundColor
{colors.primary-active}
borderColor
{colors.primary-active}
split-button-trigger-focus
outline
2px solid {colors.border-focus}
outlineOffset
2px
split-button-disabled
backgroundColor
{colors.disabled-bg}
textColor
{colors.disabled-text}
iconColor
{colors.disabled-text}
borderColor
{colors.disabled-border}
cursor
not-allowed