Split button

mandatory

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 via motion.kmd R9 + shape-library.kmd R2.

Princípios

  1. *wo distinct actions*— primary tap = default action; trailing tap = open menu. NEVER one combined.
  2. *isual separation*— divisor line entre leading + trailing.
  3. *hape morph on menu open*— trailing morphs (square → pill) + chevron rotates 180°.
  4. * 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:

  1. *hape morph trailing* from square corner → pill corner (per shape-library.kmd Pill ↔ Squircle morph).
  2. *hevron rotation* rotates 180° → via spring motion-spatial-fast.
  3. *railing color shift* optional tint to indicate active state (per preset).
  4. *enu appears* cross-link menus.kmd for 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.kmd R6.

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 XSXL: 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).

Source: ../home/koder/dev/koder/meta/docs/stack/specs/components/split-button.kmd