Split button
components specs/components/split-button.kmd
Material 3 Expressive composite button: primary action + separate menu trigger side-by-side, divided. Trailing chevron rotates + shape morphs when menu opens. 5 sizes × 4 styles. Anchored to host `buttons.kmd`.
When this spec applies
Primary triggers
- Build a button with primary action + alternate options menu
All triggers
- Action with a default + 2-3 secondary variants under a menu
- Save (default) + Save as / Export / etc.
Specification body
Spec — Split button
Companion:
buttons.kmd(base button variants). Trailing morph viamotion.kmdR9 +shape-library.kmdR2.
Princípios
- Two distinct actions — primary tap = default action; trailing tap = open menu. NEVER one combined.
- Visual separation — divisor line entre leading + trailing.
- Shape morph on menu open — trailing morphs (square → pill) + chevron rotates 180°.
- 5 sizes, 4 styles — XS/S/M/L/XL × elevated/filled/tonal/outlined.
R1 — Anatomia
┌──────────────────┬─────┐
│ Save │ ▼ │ ← leading (primary action) | divisor | trailing (menu)
└──────────────────┴─────┘
╱
╱ menu opens →
┌────────────────┐
│ Save as... │
│ Export │
│ Save & close │
└────────────────┘
Slots:
| Slot | Function |
|---|---|
| Leading | Primary action (text + optional icon). Tap = executa default. |
| Divisor | 1dp line; color outline-variant per color-roles.kmd. |
| Trailing | Menu trigger (chevron ▼). Tap = abre menu (cross-link menus.kmd). |
Both slots are focusable nodes (a11y separately reachable).
R2 — Sizes
| Size | Height | Padding (L+R per slot) | Min width (leading) | Min width (trailing) |
|---|---|---|---|---|
| XS | 28 | 8 | 60 | 28 |
| S | 32 | 10 | 72 | 32 |
| M | 40 | 16 | 88 | 40 |
| L | 48 | 20 | 104 | 48 |
| XL | 56 | 24 | 120 | 56 |
Trailing min-width ensures chevron + tap target ≥ 28dp (S+).
R3 — Styles
Same as buttons.kmd base variants:
| Style | Background | Border | Elevation |
|---|---|---|---|
| Elevated | surface tint | none | shadow + tonal |
| Filled | primary | none | none |
| Tonal | secondary-container | none | none |
| Outlined | transparent | outline 1dp | none |
Both slots share the same style (no mixed-style split buttons).
R4 — Menu open state morph
When user taps trailing → menu opens:
- Shape morph trailing: from square corner → pill corner (per
shape-library.kmdPill ↔ Squircle morph). - Chevron rotation:
▼rotates 180° →▲via springmotion-spatial-fast. - Trailing color shift: optional tint to indicate active state (per preset).
- Menu appears: cross-link
menus.kmdfor menu styling.
On menu close (selection OR ESC OR outside-tap): reverse morph.
R5 — Surface bindings
| Surface | API |
|---|---|
| Flutter | KoderSplitButton({onPrimary, menuItems, size, style}) em koder_kit/ (futuro) |
| Web | <koder-split-button size="md" style="filled"> em koder_web_kit |
| Compose Android | KoderSplitButton (futuro) |
| SwiftUI iOS | idem (futuro) |
| CLI / TUI | Plain action; menu via slash command (<action> default, <action>:menu to open) |
R6 — Acessibilidade
- Leading:
role="button" aria-label="<primary action>". - Trailing:
role="button" aria-haspopup="menu" aria-expanded="<state>" aria-label="<primary action> options". - Keyboard: Tab cycles leading → trailing; Space/Enter on each.
- Arrow Down on trailing also opens menu.
- ESC closes menu; focus returns to trailing.
- Menu items per
menus.kmdR6.
R7 — i18n
Inherits from buttons.kmd + menus.kmd. Trailing label localized as
"
| Key | en-US | pt-BR |
|---|---|---|
split_button.trailing.label | "{action} options" | "Opções de {action}" |
R8 — Reduced-motion
Shape morph + chevron rotation: instant (no spring). Menu open instant (no slide).
R9 — Per-preset variation
| Preset | Split button style |
|---|---|
material3 / material_expressive | Default morph + rotation |
material2 | No morph; chevron rotation only |
terminal_classic | [action ▾] text-only; trailing literal ▾ |
brutalist | Sharp corners; no shape morph; no rotation animation |
cyberpunk_neon | Default + glow on active state |
minimalist_mono | Single bottom-border; no fill; no morph |
T-suite
- T1 Mount: render with primary + 3 menu items → leading + divisor + trailing visible.
- T2 Primary tap: tap leading → onPrimary callback; menu NOT opened.
- T3 Menu open: tap trailing → menu visible; trailing shape morphs; chevron rotates.
- T4 Menu close: tap outside → menu closes; trailing reverts.
- T5 Keyboard: Tab to leading + Enter → onPrimary; Tab to trailing + Down → menu open.
- T6 All sizes XS-XL: render each → height correct; trailing tap-target ≥ 28dp.
- T7 All styles 4: filled/elevated/tonal/outlined render correctly.
- T8 Reduced-motion: morph + rotation instant.
- T9 A11y: leading aria-label distinct from trailing.
- N1 Click trailing accidentally → only opens menu (não executes primary action).
Cross-link
- Companion:
buttons.kmd,menus.kmd - Animation drivers:
motion.kmdR9,shape-library.kmdR2 - Color:
color-roles.kmd - Refs: M3 Split button https://m3.material.io/components/split-button/guidelines
References
specs/components/buttons.kmdspecs/components/menus.kmdspecs/themes/motion.kmdspecs/themes/shape-library.kmd