Split button
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`.
Spec — Split button
Companion:
buttons.kmd(base button variants). Trailing morph viamotion.kmdR9 +shape-library.kmdR2.
Princípios
- *wo distinct actions*— primary tap = default action; trailing tap = open menu. NEVER one combined.
- *isual separation*— divisor line entre leading + trailing.
- *hape morph on menu open*— trailing morphs (square → pill) + chevron rotates 180°.
- * sizes, 4 styles*— XSSMLXL × elevatedfilledtonal/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:
- *hape morph trailing* from square corner → pill corner (per
shape-library.kmdPill ↔ Squircle morph). - *hevron rotation*
▼rotates 180° →▲via springmotion-spatial-fast. - *railing color shift* optional tint to indicate active state (per preset).
- *enu 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 "primary options" pattern.
| 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
- *1*Mount: render with primary + 3 menu items → leading + divisor + trailing visible.
- *2*Primary tap: tap leading → onPrimary callback; menu NOT opened.
- *3*Menu open: tap trailing → menu visible; trailing shape morphs; chevron rotates.
- *4*Menu close: tap outside → menu closes; trailing reverts.
- *5*Keyboard: Tab to leading + Enter → onPrimary; Tab to trailing + Down → menu open.
- *6*All sizes XS
XL: render each → height correct; trailing taptarget ≥ 28dp. - *7*All styles 4: filledelevatedtonal/outlined render correctly.
- *8*Reduced-motion: morph + rotation instant.
- *9*A11y: leading aria-label distinct from trailing.
- *1*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