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.
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-expandedreflects 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