FAB menu
components specs/components/fab-menu.kmd
Material 3 Expressive FAB menu — successor of legacy "speed-dial". ToggleFloatingActionButton + N FloatingActionButtonMenuItems with staggered spring expansion + shape morph (FAB → "X"). Max 6 items (above → migrate to Navigation drawer).
When this spec applies
Primary triggers
- FAB needs to expose multiple secondary actions
All triggers
- Compose-style entry point with 2-6 related actions
- Replace legacy speed-dial in Koder app
Specification body
Spec — FAB menu
Companion:
buttons.kmdFAB section. Animation viamotion.kmdR9.1 spring +shape-library.kmdR2 morph.
Princípios
- 2-6 items — < 2 → use single FAB. > 6 → migrate to Navigation drawer.
- Staggered expansion — items appear in sequence (50-100ms offset) via spring.
- Toggle morph — main FAB morphs FAB → "X" shape on open; reverse on close.
- Anchored above by default — fan upward; auto-flip if window orientation requires.
R1 — Anatomia
Item 3 ⬡ ← label
Item 2 ⬡
Item 1 ⬡
┌──────────────────┐
│ [×] │ ← toggle FAB (morphed)
└──────────────────┘
Slots:
| Slot | Function |
|---|---|
| Toggle FAB | Primary. Tap = open/close menu; shape morph between FAB (default) and "X" (open). |
| Menu items | 1-6 secondary actions; each a small FAB + label chip. |
R2 — Sizes
Toggle FAB sizes inherit buttons.kmd FAB:
- Small (40dp) / Default (56dp) / Large (96dp).
Menu items: small FAB only (40dp) — visually subordinate.
R3 — Anchor + direction
Default: above (fan upward from toggle).
Auto-flip rules:
| Window orientation/class | Direction |
|---|---|
| Compact portrait (mobile) | Above (default) |
| Compact landscape (mobile) | Above (still — UX consistency) |
| Medium/Expanded (tablet/desktop) | Beside (left-of-toggle if RTL; right-of-toggle if LTR) |
| Toggle at top of viewport | Force below (auto-flip) |
| Toggle at side | Force opposite direction |
R4 — Expansion animation
On tap toggle (open):
- Toggle FAB morph: FAB shape → "X" (Cookie-4 → diagonal cross via
shape-library.kmdCookie-4 → Diamond morph). - Items appear: stagger 60ms offset per item; each via
motion-spatial-fastspring (slight overshoot). - Item labels: fade-in via
motion-effect-fast. - Optional scrim: backdrop tint
surface-dim0.4 alpha (configurable).
On tap toggle (close) OR outside tap OR item selection OR ESC:
- Reverse — items fade-out in reverse order (stagger 30ms; faster than open).
- Toggle morph back: "X" → FAB shape.
- Scrim fades out.
R5 — Threshold + alternatives
| Item count | Recommended pattern |
|---|---|
| 1 | Single FAB (no menu) |
| 2-6 | FAB menu (this spec) |
| 7+ | Migrate to Navigation drawer ou Bottom sheet |
Anti-pattern: hiding important destructive actions inside FAB menu (use Confirm dialog instead).
R6 — Surface bindings
| Surface | API |
|---|---|
| Flutter | KoderFabMenu({toggleIcon, items, onItemTap, direction}) em koder_kit/ (futuro) |
| Web | <koder-fab-menu> em koder_web_kit |
| Compose Android | KoderFabMenu (futuro) |
| SwiftUI iOS | idem (futuro) |
| CLI / TUI | n/a |
R7 — Acessibilidade
- Toggle:
role="button" aria-haspopup="menu" aria-expanded="<state>" aria-label="More actions". - Items:
role="menuitem" aria-label="<action>". - Keyboard: Tab to toggle + Enter opens; Arrow Up/Down navigates items; Enter selects; ESC closes.
- Focus trap: while open, Tab cycles within items + toggle only.
- Screen reader announces toggle state change.
R8 — i18n
| Key | en-US | pt-BR |
|---|---|---|
fab_menu.toggle.label_closed | "More actions" | "Mais ações" |
fab_menu.toggle.label_open | "Close menu" | "Fechar menu" |
R9 — Reduced-motion
Open/close: instant. Toggle morph: skip; show static "X" icon swap. Staggered appearance: replaced by simultaneous fade-in (no stagger).
R10 — Per-preset variation
| Preset | FAB menu style |
|---|---|
material3 / material_expressive | Default morph + stagger spring |
material2 | Simple icon swap + linear stagger (no spring) |
terminal_classic | List below toggle (no animation) |
brutalist | Sharp items; no morph |
cyberpunk_neon | Default + glow on items + connection lines toggle ↔ items |
minimalist_mono | No labels (icon-only); ultra-spartan |
T-suite
- T1 Mount closed: toggle FAB visible; items hidden.
- T2 Open: tap toggle → items appear staggered; toggle morphs to X.
- T3 Item count 3: render 3 items vertically above toggle.
- T4 Direction auto-flip: position toggle at top edge → menu opens BELOW.
- T5 Tablet landscape: render direction = beside.
- T6 Item select: tap item → menu closes; onItemTap fires.
- T7 Outside tap closes: tap scrim → menu closes.
- T8 ESC closes: menu open + ESC → closes.
- T9 Keyboard nav: arrows + Enter functional.
- T10 Reduced-motion: instant open/close.
- T11 A11y: focus trap; aria-expanded transitions.
- N1 > 6 items: spec validation OR runtime warning suggesting Navigation drawer.
Cross-link
- Companion:
buttons.kmdFAB variants - Animation drivers:
motion.kmdR9.1,shape-library.kmdR2 - Layout:
app-layout/window-size-classes.kmd(direction rules) - Migration when > 6 items:
navigation.kmddrawer - Refs: M3 FAB menu pattern; legacy speed-dial replaced
Requirements (testable)
Requirement: Toggle opens and closes the menu with shape morph {#req-fab-menu-toggle-morph}
The toggle FAB SHALL open the menu on tap when closed and close it on tap when open, and while opening it SHALL morph its shape from the FAB shape to the "X" shape (reversing on close).
Scenario: Tapping the closed toggle opens the menu
- GIVEN a FAB menu mounted in the closed state with the toggle FAB visible and its menu items hidden
- WHEN the user taps the toggle FAB
- THEN the menu items become visible
- AND the toggle FAB shape morphs from the FAB shape to the "X" shape
- AND
aria-expandedon the toggle readstrue
Scenario: Tapping the open toggle closes the menu
- GIVEN a FAB menu in the open state showing its menu items
- WHEN the user taps the toggle FAB again
- THEN the menu items become hidden
- AND the toggle FAB shape morphs from the "X" shape back to the FAB shape
Requirement: Item count stays within the 2-6 range {#req-fab-menu-item-count}
The FAB menu SHALL be used only for 2 to 6 secondary actions; below 2 items a single FAB SHALL be used instead, and above 6 items the component SHALL emit a validation error or runtime warning recommending migration to a Navigation drawer.
Scenario: Rendering three items produces three menu entries
- GIVEN a FAB menu configured with exactly 3 items
- WHEN the menu is opened
- THEN 3 menu items are rendered vertically anchored to the toggle FAB
Scenario: Exceeding six items raises a warning
- GIVEN a FAB menu configured with 7 items
- WHEN the component is validated or mounted
- THEN a validation error or runtime warning is emitted
- AND the warning message recommends migrating to a Navigation drawer
Requirement: Menu direction defaults above and auto-flips by position and window class {#req-fab-menu-direction}
The menu SHALL fan upward above the toggle by default, SHALL force the opposite direction when the toggle is at a viewport edge, and SHALL open beside the toggle on Medium or Expanded window size classes.
Scenario: Toggle at the top edge opens the menu below
- GIVEN a FAB menu whose toggle FAB is positioned at the top edge of the viewport
- WHEN the menu is opened
- THEN the menu items fan downward below the toggle instead of above it
Scenario: Expanded window class opens the menu beside the toggle
- GIVEN a FAB menu rendered in a Medium or Expanded window size class (tablet/desktop) in an LTR layout
- WHEN the menu is opened
- THEN the menu items are anchored beside the toggle to the right of it
Requirement: Menu closes on item select, outside tap, or ESC {#req-fab-menu-close-triggers}
While open, the menu SHALL close on selection of a menu item, on a tap outside the menu (scrim), or on the ESC key, and item selection SHALL invoke the item's tap callback.
Scenario: Selecting an item closes the menu and fires the callback
- GIVEN a FAB menu in the open state
- WHEN the user taps one of the menu items
- THEN the menu closes and its items become hidden
- AND the
onItemTapcallback fires for the selected item
Scenario: ESC closes the open menu
- GIVEN a FAB menu in the open state with keyboard focus inside it
- WHEN the user presses the ESC key
- THEN the menu closes and its items become hidden
Requirement: Accessibility roles, keyboard navigation, and focus trap {#req-fab-menu-a11y}
The toggle SHALL expose role="button", aria-haspopup="menu", and an aria-expanded state that tracks open/closed; each item SHALL expose role="menuitem"; and while the menu is open keyboard focus SHALL be trapped so that Tab cycles only within the items and toggle, with Arrow Up/Down navigating items and Enter selecting.
Scenario: Arrow keys navigate items and aria-expanded tracks state
- GIVEN a FAB menu that has just been opened via the keyboard so
aria-expandedistrue - WHEN the user presses Arrow Down
- THEN keyboard focus moves to the next menu item in sequence
- AND each item carries
role="menuitem"with its actionaria-label
Scenario: Focus is trapped within the open menu
- GIVEN a FAB menu in the open state with focus on the last menu item
- WHEN the user presses Tab
- THEN focus cycles back to the toggle or first item and does not leave the menu
Requirement: Reduced-motion disables morph and stagger {#req-fab-menu-reduced-motion}
When reduced-motion is active, the menu SHALL open and close instantly, SHALL skip the toggle shape morph in favor of a static "X" icon swap, and SHALL replace the staggered item appearance with a simultaneous fade-in.
Scenario: Opening under reduced-motion is instant without stagger
- GIVEN a FAB menu rendered with reduced-motion active
- WHEN the user taps the toggle to open the menu
- THEN all menu items appear simultaneously without staggered timing
- AND the toggle presents a static "X" icon swap rather than a shape morph animation
References
specs/components/buttons.kmdspecs/themes/motion.kmdspecs/themes/shape-library.kmdspecs/app-layout/window-size-classes.kmd