---
title: "FAB menu"
description: "Opens a short list of related actions from a toggle drawn as a collapsed main action button, which turns into the list's close button while they show."
url: "https://tanrei.chnotchy.com/components/fab-menu/"
last_updated: 2026-10-23T21:17:12.000Z
---

# FAB menu

> Opens a short list of related actions from a toggle drawn as a collapsed main action button, which turns into the list's close button while they show.

## Overview

### Usage

The toggle is drawn as a collapsed [main action button](https://tanrei.chnotchy.com/components/main-action-button/), with a label and an icon. The items are actions, each an icon and a label, passed the way each platform passes repeated children. Like the main action button, the FAB menu does not place itself: your layout puts the toggle, usually in the inline-end bottom corner. The items appear above it, in the platform’s overlay layer.

**Web**

```html
<tanrei-fab-menu class="primary-action" label="Create">
  <span slot="icon">add</span>
  <tanrei-fab-menu-item label="Document" icon="description"></tanrei-fab-menu-item>
  <tanrei-fab-menu-item label="Folder" icon="create_new_folder"></tanrei-fab-menu-item>
  <tanrei-fab-menu-item label="Upload" icon="upload"></tanrei-fab-menu-item>
</tanrei-fab-menu>

<style>
  /* The element places nothing: the page puts the toggle in the inline-end bottom corner. */
  tanrei-fab-menu.primary-action {
    position: fixed;
    inset-inline-end: var(--tanrei-sys-space-space400);
    inset-block-end: var(--tanrei-sys-space-space400);
  }
</style>

<script type="module">
  // The FAB menu lives in the m3-like layer: the entry of an m3-like system registers it with the rest.
  import '@tanrei/web/m3-like';
  const fabMenu = document.querySelector('tanrei-fab-menu');
  // The toggle asks to open or close; you set open.
  fabMenu.addEventListener('toggle', (e) => (fabMenu.open = e.detail.open));
  // close carries the reason: escape, outside, tab, focus-out or item. The web never sends back.
  fabMenu.addEventListener('close', () => (fabMenu.open = false));
  // An item's click bubbles, and comes before close.
  fabMenu.querySelector('[label="Upload"]').addEventListener('click', upload);
</script>
```

**React**

```tsx
// The FAB menu lives in the m3-like layer: it is in the entries of the systems that have it (m3-like, m3-expressive-like), and in /all.
import { FabMenu, FabMenuItem } from '@tanrei/react/m3-like';

<FabMenu
  className="primary-action"
  label="Create"
  open={open}
  onToggle={(e) => setOpen(e.detail.open)}
  onClose={() => setOpen(false)}
>
  <span slot="icon">add</span>
  <FabMenuItem label="Document" icon="description" onClick={newDocument} />
  <FabMenuItem label="Folder" icon="create_new_folder" onClick={newFolder} />
  <FabMenuItem label="Upload" icon="upload" onClick={upload} />
</FabMenu>
```

**SwiftUI**

```swift
import Tanrei

// Placed as the FAB is. The toggle and the items both write the binding.
DocumentList()
    .overlay(alignment: .bottomTrailing) {
        TanreiFabMenu("Create", icon: "add", isPresented: $showsCreate) {
            TanreiFabMenuItem("Document", icon: "description", action: newDocument)
            TanreiFabMenuItem("Folder", icon: "create_new_folder", action: newFolder)
            TanreiFabMenuItem("Upload", icon: "upload", action: upload)
        }
        .padding()
    }
```

**Compose**

```kotlin
import io.github.chnotchy.tanrei.components.TanreiFabMenu
import io.github.chnotchy.tanrei.components.TanreiFabMenuItem
import io.github.chnotchy.tanrei.primitives.TanreiText

// State hoisting: you keep expanded. Put it in a scaffold's floatingActionButton slot, or place it yourself.
TanreiFabMenu(
    expanded = expanded,
    onExpandedChange = { expanded = it },
    onDismissRequest = { expanded = false },
    contentDescription = "Create",
    icon = { TanreiText("add") },
) {
    TanreiFabMenuItem(text = "Document", icon = "description", onClick = ::newDocument)
    TanreiFabMenuItem(text = "Folder", icon = "create_new_folder", onClick = ::newFolder)
    TanreiFabMenuItem(text = "Upload", icon = "upload", onClick = ::upload)
}
```

**Flutter**

```dart
import 'package:tanrei/tanrei.dart';

// The FAB menu opens and closes itself through the controller; onToggle and onClose report it.
Scaffold(
  body: const DocumentList(),
  floatingActionButton: TanreiFabMenu(
    controller: createMenu,
    semanticLabel: 'Create',
    icon: const Icon(AppSymbols.add),
    items: <TanreiFabMenuItem>[
      TanreiFabMenuItem(label: 'Document', icon: const Icon(AppSymbols.description), onPressed: newDocument),
      TanreiFabMenuItem(label: 'Folder', icon: const Icon(AppSymbols.createNewFolder), onPressed: newFolder),
      TanreiFabMenuItem(label: 'Upload', icon: const Icon(AppSymbols.upload), onPressed: upload),
    ],
  ),
);
```

**React Native**

```tsx
import { View } from 'react-native';
import { FabMenu } from '@tanrei/react-native';

// Controlled: set visible from onToggle, and to false from onDismiss, which receives the reason.
// The toggle never positions itself: style places it.
<View style={{ flex: 1 }}>
  <DocumentList />
  <FabMenu
    icon="add"
    accessibilityLabel="Create"
    visible={open}
    onToggle={setOpen}
    onDismiss={() => setOpen(false)}
    items={[
      { label: 'Document', icon: 'description', onPress: newDocument },
      { label: 'Folder', icon: 'create_new_folder', onPress: newFolder },
      { label: 'Upload', icon: 'upload', onPress: upload },
    ]}
    style={styles.primaryAction}
  />
</View>
```

## Specs

- Category: Actions
- Level: base
- Listing: default
- Lives in the `m3-like` layer
- Page: https://tanrei.chnotchy.com/components/fab-menu/
- Spec: https://github.com/chnotchy/tanrei/blob/main/specs/fab-menu.spec.json

### Availability

| Design system | Has it | Notes |
| --- | --- | --- |
| Tanrei (`tanrei`) | No | It lives in the `m3-like` layer, which this system does not extend; the systems that have it: `m3-expressive-like`, `m3-like`. Instead use: Main action button, Bottom sheet. A system outside this layer draws no floating action button that opens a list: its main action button opens a bottom sheet that lists the related actions. |
| Cupertino (Apple HIG-inspired) (`cupertino`) | No | It lives in the `m3-like` layer, which this system does not extend; the systems that have it: `m3-expressive-like`, `m3-like`. Instead use: Main action button, Bottom sheet. A system outside this layer draws no floating action button that opens a list: its main action button opens a bottom sheet that lists the related actions. |
| M3 Expressive-like (Material 3 Expressive-inspired) (`m3-expressive-like`) | Yes | — |
| M3-like (Material 3-inspired) (`m3-like`) | Yes | — |

### Platforms

- Web: the element `<tanrei-fab-menu>`, registered by importing `@tanrei/web/m3-expressive-like` (or only its own module, `@tanrei/web/components/fab-menu`).
- React: `FabMenu` from `@tanrei/react/m3-expressive-like`.

| Platform | Name | Status |
| --- | --- | --- |
| Web | `tanrei-fab-menu` | lagging |
| SwiftUI | `TanreiFabMenu` | lagging |
| Jetpack Compose | `TanreiFabMenu` | lagging |
| Flutter | `TanreiFabMenu` | lagging |
| React Native | `FabMenu` | lagging |

Names a platform gives in its own idiom:

| Spec | Web | SwiftUI | Jetpack Compose | Flutter | React Native |
| --- | --- | --- | --- | --- | --- |
| `open` (prop) | `open` | `isPresented` | `expanded` | `controller` | `visible` |
| `label` (prop) | `label` | `label` | `contentDescription` | `semanticLabel` | `accessibilityLabel` |
| `disabled` (prop) | `disabled` | `disabled` | `enabled` | `enabled` | `disabled` |
| `toggle` (event) | `toggle` | `isPresented` | `onExpandedChange` | `onToggle` | `onToggle` |
| `close` (event) | `close` | `isPresented` | `onDismissRequest` | `onClose` | `onDismiss` |

Statuses: `planned` — In the inventory. Nothing shippable exists yet. `lagging` — An implementation of an earlier spec exists; it has not caught up with the current one. `alpha` — Implemented with tests. The API may still change. `beta` — Feature complete and at parity. Only fixes are expected. `stable` — Covered by semantic versioning. `deprecated` — Still shipped; scheduled for removal.

### Anatomy

- `container`: The toggle that opens and closes the menu, drawn as a collapsed main action button. It casts the shadow, carries hover, focus and press, and changes its colors and corners between closed and open.
- `icon`: The toggle's glyph: the product's icon while closed, the glyph close of the system's icon set while open. The FAB menu draws no mark of its own for it.
- `list`: The column of items above the toggle, end-aligned with it. It draws nothing of its own and is presented above the page.
- `item`: One action: a raised container holding an icon and a label. It carries its own hover, focus and press.
- `item-icon`: An item's glyph, before its label.
- `item-label`: An item's text and its accessible name.
- `focus-ring`: Gap-then-ring indicator shown for keyboard focus, around the toggle or the item that has it.

### Props

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `variant` | `enum`: `filled`, `tonal`, `surface` | `"filled"` | `variant` | Emphasis, as the main action button's variants of the same names: it colors the toggle and the items. A design system may add values. |
| `size` | `enum`: `small`, `medium`, `large` | `"medium"` | `size` | The toggle's size (40 / 48 / 64, square), as the main action button's. The items keep one size. A design system may add values. |
| `open` | `boolean` | `false` | `open` | Whether the items are shown. Where open is a plain value (web, Compose, React Native) the product sets it from toggle and sets it to false from close; where it is a binding or a controller (SwiftUI, Flutter) the FAB menu opens and closes itself through it (FBM-1, FBM-15). |
| `label` | `string` | `""` | `label` | The accessible name of the toggle and of the list ("Create"). Required: an icon alone is not a name. |
| `disabled` | `boolean` | `false` | `disabled` | Prevents opening and removes the toggle from the focus order. Hiding a FAB menu whose actions are unavailable is usually better than disabling it. |

### Events

| Event | Description |
| --- | --- |
| `toggle` | The toggle asks to open or close the menu; carries the open value it asks for. |
| `close` | The menu asks to be closed; carries the reason: escape, back, outside, tab, focus-out or item (in the platform's casing). |

### Slots

| Slot | Description |
| --- | --- |
| `icon` | The toggle's glyph while closed (a glyph of the system's icon set). |

### Item: `item`

One action of the menu: an icon and a label on a raised container.

Names: Web `tanrei-fab-menu-item`, SwiftUI `TanreiFabMenuItem`, Jetpack Compose `TanreiFabMenuItem`, Flutter `TanreiFabMenuItem`, React Native `FabMenuItem`.

| Prop | Type | Default | Web attribute | Description |
| --- | --- | --- | --- | --- |
| `label` | `string` | `""` | `label` | The item's text and its accessible name. Required. |
| `icon` | `string` | `""` | `icon` | The name of the item's icon (a glyph of the system's icon set). Required. |

| Event | Description |
| --- | --- |
| `click` | The item was activated by pointer, touch, keyboard or assistive technology. |

### States

`enabled`, `disabled`, `hovered`, `focused`, `pressed`

### Accessibility

- Role: `button`
- Labeling: label names the toggle and the list ("Create"). Each item is named by its label. Icons are decorative. The toggle keeps its name while open; its expanded state says it is open.
- Minimum target size: yes

| Keys | Action |
| --- | --- |
| Tab / Shift+Tab | Moves focus to the toggle; from an item, closes the menu and moves focus on from the toggle. |
| Enter, Space or Down Arrow on the toggle | Opens the menu (Enter and Space also close it); focus moves to the first item. |
| Up Arrow on the toggle | Opens the menu; focus moves to the last item, the one nearest the toggle. |
| Down Arrow / Up Arrow | Moves focus to the next or previous item, wrapping at the ends. |
| Home / End | Moves focus to the first or last item. |
| Enter or Space on an item | Activates the item and closes the menu. |
| Escape | Closes the menu and returns focus to the toggle. |

- Focus: the FAB menu is an operated overlay that is not modal, like the menu. It moves focus in when it opens and returns it to the toggle when it closes, but does not keep it: Tab from an item closes it.
- Its place in the focus and reading order is where the layout puts the toggle in the element tree, not where it is drawn, as for the main action button.
- Compose has no menu role: the items are buttons in a traversal group named by label, with their position in their collection info.
- Text size does not change the toggle: an icon is not text. The items grow with enlarged text and wrap their labels.
- How a state looks is a value decision of the active design system; components read the state's tokens and never compute the effect themselves.

### Behaviour

Requirements in EARS form, each with its stable id.

- **FBM-1** WHEN the user activates the toggle by pointer, touch, Enter or Space THE FAB menu SHALL emit toggle exactly once with the opposite of open, and WHEN the user presses Down Arrow or Up Arrow on the toggle while open is false it SHALL emit toggle with true; the menu SHALL then open or close through open: through its binding or controller where open is one (SwiftUI, Flutter), and by the product setting open where open is a plain value (web, Compose, React Native).
- **FBM-2** WHILE disabled is true THE FAB menu SHALL NOT emit toggle or close, SHALL NOT make the toggle focusable, SHALL expose the disabled state to assistive technology, and SHALL draw the toggle closed and present no items, whatever open says.
- **FBM-3** WHILE the toggle or an item is hovered, keyboard-focused or pressed THE FAB menu SHALL render its container, icon, label, elevation and opacity from the component tokens of its variant in its own highest-precedence active state — the toggle from its closed parts (comp.fab-menu.filled.container.color for filled, and so on) or its open parts (comp.fab-menu.filled.open.container.color, and so on), an item from the item parts (comp.fab-menu.filled.item.container.color, and so on) — with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state.
- **FBM-4** WHEN the toggle or an item receives keyboard focus THE FAB menu SHALL show the focus ring around that one's own shape, without animation; focus gained by pointer or touch SHALL NOT show it.
- **FBM-5** WHERE the toggle is smaller than the minimum target THE FAB menu SHALL extend its hit area on both axes to sys.target.touch for touch input and sys.target.pointer for fine pointers, without changing its visual size; THE FAB menu SHALL make each item at least that target tall, because stacked items cannot extend a hit area over each other, and each item's whole container SHALL be its hit area; a platform that cannot tell the input type SHALL use sys.target.touch.
- **FBM-6** WHILE disabled is true THE FAB menu SHALL render the toggle's container, icon, elevation and opacity from the disabled component tokens of its variant's closed parts.
- **FBM-7** WHEN variant or size names a value that the active design system does not have THE FAB menu SHALL render with the default value's tokens, and in debug builds report the unknown value.
- **FBM-8** WHEN the toggle is pressed by pointer, touch, Enter or Space THE toggle SHALL show the pressed state without delay and SHALL keep it visible for at least sys.motion.duration.press-hold, even when the press ends sooner.
- **FBM-9** WHILE pressed THE toggle SHALL answer as sys.behavior.pressed-feedback names: overlay applies the pressed tokens to the whole container at once; ripple grows the pressed state layer from the point of contact (from the center for keyboard activation); dim applies the pressed opacity to the whole control and draws no state layer; none draws no answer to the press. Under ripple and dim only the container color stays with the state under the press; icon, elevation, opacity and shape still come from the pressed tokens. Under none every part stays with the state under the press. The ripple grows over comp.fab-menu.motion.feedback-duration with comp.fab-menu.motion.state-easing, fades over comp.fab-menu.motion.state-duration with comp.fab-menu.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced. Each item SHALL answer its own press in the same way, within its own container and from its item parts: label, icon, elevation and opacity still come from its pressed tokens.
- **FBM-10** THE toggle SHALL ease its container corners by sys.shape.corner-smoothing (0 is a quarter circle, 0.6 the iOS continuous corner; a platform with only the OS curve draws it for any amount above 0) and show its keyboard focus as sys.behavior.focus-style names (ring or glow); a platform that cannot draw a choice SHALL fall back to the first value and state that in its README. Each item SHALL ease its corners and show its keyboard focus in the same way.
- **FBM-11** THE FAB menu SHALL draw the toggle as a square whose side is the container size of its size (comp.fab-menu.container.size.medium for medium, and so on), with the corners of its size (comp.fab-menu.container.shape.medium, and so on) while closed and comp.fab-menu.container.shape-open while open, and its icon centred in it at the icon size of its size (comp.fab-menu.icon.size.medium, and so on) through the icon helper, with the family and fill of an unselected icon, sys.icon.weight, and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max: the icon slot's glyph while closed, and the glyph close of the system's icon set while open, not mirrored when the layout direction is right-to-left. THE FAB menu SHALL draw no mark of its own (no path, shape or character of its own) for the close glyph.
- **FBM-12** THE FAB menu SHALL NOT position itself: the toggle SHALL lay out at its own size where the layout that holds it places it (normally the inline-end bottom corner of the screen, clear of any navigation bar), as a main action button does (MAB-18), and the items SHALL be presented above the page rather than laid out, so that they move no content.
- **FBM-13** WHEN open becomes true THE FAB menu SHALL present its items in the platform's overlay layer (the web's top layer through the popover API, SwiftUI's presentation as the menu's, Compose's Popup, Flutter's Overlay, React Native's overlay layer that TanreiProvider renders) as a column above the toggle, comp.fab-menu.list.offset from its top edge and comp.fab-menu.list.gap apart, the first item at the top and every item's end edge aligned with the toggle's, and SHALL move focus to the first item, or to the last (the one nearest the toggle) when Up Arrow opened it; WHEN open becomes false THE FAB menu SHALL remove the items and, if focus was in them, return focus to the toggle. The FAB menu is an operated overlay, so it takes focus and returns it although it is not modal.
- **FBM-14** WHILE open is true THE FAB menu SHALL NOT trap focus, SHALL NOT make the rest of the screen inert and SHALL draw no scrim: it is an operated overlay that is not modal.
- **FBM-15** WHEN the user presses Escape or the platform's back, presses outside the items and the toggle, presses Tab or Shift+Tab on an item, or moves focus out of the items and the toggle THE FAB menu SHALL emit close once with that reason (escape, back, outside, tab, focus-out) and SHALL be closed: through its binding or controller where open is one (SwiftUI, Flutter), and by the product setting open to false where open is a plain value (web, Compose, React Native); Tab and Shift+Tab SHALL then move focus on from the toggle as they would have from it. A press on the toggle while open is not a press outside: it SHALL emit toggle with false (FBM-1) and no close. Whether a press outside also reaches the element under it SHALL follow the platform, as the menu's does, and be stated in its README.
- **FBM-16** WHEN the user activates an item by pointer, touch, Enter or Space THE FAB menu SHALL emit the item's click once and then emit close with the reason item, in that order, and SHALL be closed as FBM-15 says.
- **FBM-17** WHILE open is true and an item has keyboard focus THE FAB menu SHALL move focus to the next item with Down Arrow and to the previous one with Up Arrow, wrapping at the ends, and to the first and last item with Home and End.
- **FBM-18** THE FAB menu SHALL draw each item as a container at least comp.fab-menu.item.height tall and as wide as its content, with corners comp.fab-menu.item.shape eased by sys.shape.corner-smoothing, holding its icon and then its label comp.fab-menu.item.gap apart, padded by comp.fab-menu.item.padding-inline-start at the start and comp.fab-menu.item.padding-inline-end at the end; the label SHALL be set in comp.fab-menu.item.label.typescale on one line while it fits and SHALL wrap rather than truncate, the item growing with it; the icon SHALL be drawn at comp.fab-menu.item.icon.size through the icon helper with the family and fill of an unselected icon, sys.icon.weight, and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max; and the item SHALL cast its item elevation (comp.fab-menu.filled.item.container.elevation for filled, and so on) outside its container without clipping it.
- **FBM-19** WHEN the FAB menu opens THE FAB menu SHALL change the toggle's colors and corners and cross-fade its icon to the glyph close (a cross-fade of two glyphs, neither drawn along a path) over comp.fab-menu.motion.open-duration with comp.fab-menu.motion.open-easing, and fade and grow each item in from its end edge over the same, starting with the item nearest the toggle and delaying each next one by comp.fab-menu.motion.stagger; WHEN it closes it SHALL reverse the toggle's change and fade all items out together over comp.fab-menu.motion.close-duration with comp.fab-menu.motion.close-easing; WHERE motion is reduced it SHALL change and show or hide everything at once.
- **FBM-20** THE FAB menu SHALL expose the toggle as a button named by label that opens a menu, with whether the menu is open where the platform has those properties, and while open SHALL expose the items as a menu named by label, each a menu item named by its own label with its position and the number of items; the icons SHALL be hidden.
- **FBM-21** THE FAB menu SHALL cast the toggle's elevation, of its variant and state (comp.fab-menu.filled.container.elevation for filled at rest, comp.fab-menu.filled.open.container.elevation while open, and so on), outside its container without clipping it, and WHEN the elevation or the colors of the toggle or an item change between interaction states SHALL animate the change over comp.fab-menu.motion.state-duration with comp.fab-menu.motion.state-easing, changing them at once where motion is reduced.
- **FBM-22** WHEN the layout direction is right-to-left THE FAB menu SHALL align the items' end edges with the toggle's left edge, mirror the order of each item's icon and label (a glyph itself only as FBM-26 says) and apply its paddings by logical start and end, and SHALL grow a ripple from the point of contact as in left-to-right.
- **FBM-23** WHILE label is not empty THE FAB menu SHALL expose it as the toggle's accessible name, over a name the product gives it on the element (aria-label or aria-labelledby). WHILE label is empty and the product gives the FAB menu a name of its own on the element — aria-label, or aria-labelledby naming an element — THE FAB menu SHALL expose that name as the toggle's accessible name and name the list by the toggle. WHILE label is empty and the product gives it none, an item's label or icon is empty, or the menu holds fewer than two or more than six items, THE FAB menu SHALL still render, and in debug builds report it.
- **FBM-24** IF the icon set cannot draw the close glyph yet (its font is still loading or failed to load) WHILE open is true THE FAB menu SHALL draw nothing in the toggle's icon box, never the glyph's name as text, SHALL keep the toggle's size, colors, corners, name and expanded state, and SHALL draw the glyph once the icon set can, without the cross-fade of FBM-19.
- **FBM-25** THE FAB menu SHALL take its layout direction from where it is used — the direction of the nearest ancestor that sets one, and the platform's own where none does — and SHALL have no direction setting of its own; WHEN that direction changes while the FAB menu is shown THE FAB menu SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state and its focus.
- **FBM-26** WHERE the layout direction is right-to-left THE FAB menu SHALL draw the glyph of its icon slot while closed and the icon of each item mirrored left to right exactly when the system's icon set marks that glyph's name as mirrored or the product forces mirroring for that use, and unmirrored when the product forbids it: the icon helper mirrors the glyph inside its own box, about the box's vertical centre line, changing neither its place, size, color and fill nor the order FBM-22 gives. THE FAB menu SHALL NOT mirror a glyph itself, and SHALL NOT mirror content the product gives as an image or a view of its own rather than by a glyph's name. The close glyph of the open toggle is not marked and is never mirrored (FBM-11).
- **FBM-27** THE FAB menu SHALL lay out the label of each item as text of its own direction, isolated from the text around it: each takes the base direction of its first strong character, or the layout direction where it has none, so that a left-to-right word, a number with its sign or unit, a telephone number and an address keep their own order in a right-to-left layout and reorder nothing beside them; THE FAB menu SHALL still place each label and align its lines by the layout direction, not by the text's own.
- **FBM-28** WHERE the platform has ARIA attributes (the web), WHEN the product sets an aria- attribute on the host, by the attribute or by the ARIA property that reflects it, THE FAB menu SHALL move it to the data-aria- attribute of the same name, so that the host is no second node with its name, description or state, and SHALL expose it on the element that carries its toggle's role — an id reference by the elements it names in the host's tree or a tree around it, an error message as part of the description while the toggle is invalid — except those the FAB menu owns (aria-haspopup, aria-expanded, aria-controls, aria-disabled), which it SHALL move and not expose. WHEN the product removes the aria- attribute or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE FAB menu SHALL take it off that element. An item's host carries its menu item role itself (FBM-20), so what the product writes on it is not moved, but for a name: WHILE an item's label is not empty THE FAB menu SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that label is the item's whole name although the platform gives aria-labelledby the precedence, and WHEN label is emptied SHALL give them back as they were written; WHILE an item's label is empty a name the product gives so is the item's name and SHALL NOT be reported as having none in debug builds (FBM-23); WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it to null, THE FAB menu SHALL take it away. WHEN the product sets an aria- attribute the FAB menu owns THE FAB menu SHALL report it in debug builds.

### Tokens

As written in the token documents (DTCG): an alias in braces names the token it takes its value from, which each design system resolves. On the web each token is a CSS custom property: `--tanrei` and the name after `tanrei`, joined by hyphens (`tanrei.comp.fab-menu.…` → `--tanrei-comp-fab-menu-…`).

#### `tanrei.comp.fab-menu`

| Token | Type | Value |
| --- | --- | --- |
| `comp.fab-menu.container.size.small` | `dimension` | `40px` |
| `comp.fab-menu.container.size.medium` | `dimension` | `48px` |
| `comp.fab-menu.container.size.large` | `dimension` | `64px` |
| `comp.fab-menu.container.shape.small` | `dimension` | `{tanrei.sys.shape.corner.medium}` |
| `comp.fab-menu.container.shape.medium` | `dimension` | `{tanrei.sys.shape.corner.large}` |
| `comp.fab-menu.container.shape.large` | `dimension` | `{tanrei.sys.shape.corner.extra-large}` |
| `comp.fab-menu.container.shape-open` | `dimension` | `{tanrei.sys.shape.corner.full}` |
| `comp.fab-menu.icon.size.small` | `dimension` | `{tanrei.sys.icon.small}` |
| `comp.fab-menu.icon.size.medium` | `dimension` | `{tanrei.sys.icon.medium}` |
| `comp.fab-menu.icon.size.large` | `dimension` | `{tanrei.sys.icon.large}` |
| `comp.fab-menu.item.height` | `dimension` | `48px` |
| `comp.fab-menu.item.shape` | `dimension` | `{tanrei.sys.shape.corner.large}` |
| `comp.fab-menu.item.padding-inline-start` | `dimension` | `{tanrei.sys.space.space200}` |
| `comp.fab-menu.item.padding-inline-end` | `dimension` | `{tanrei.sys.space.space250}` |
| `comp.fab-menu.item.gap` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.fab-menu.item.label.typescale` | `typography` | `{tanrei.sys.typescale.label}` |
| `comp.fab-menu.item.icon.size` | `dimension` | `{tanrei.sys.icon.small}` |
| `comp.fab-menu.list.gap` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.fab-menu.list.offset` | `dimension` | `{tanrei.sys.space.space100}` |
| `comp.fab-menu.filled.container.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.fab-menu.filled.container.elevation` | `shadow` | `{tanrei.sys.elevation.level3}` |
| `comp.fab-menu.filled.icon.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.fab-menu.filled.state-layer.color` | `color` | `{tanrei.comp.fab-menu.filled.icon.color}` |
| `comp.fab-menu.filled.open.container.color` | `color` | `{tanrei.sys.color.secondary-container}` |
| `comp.fab-menu.filled.open.container.elevation` | `shadow` | `{tanrei.sys.elevation.level3}` |
| `comp.fab-menu.filled.open.icon.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.fab-menu.filled.open.state-layer.color` | `color` | `{tanrei.comp.fab-menu.filled.open.icon.color}` |
| `comp.fab-menu.filled.item.container.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.fab-menu.filled.item.container.elevation` | `shadow` | `{tanrei.sys.elevation.level2}` |
| `comp.fab-menu.filled.item.label.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.fab-menu.filled.item.icon.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.fab-menu.filled.item.state-layer.color` | `color` | `{tanrei.comp.fab-menu.filled.item.label.color}` |
| `comp.fab-menu.tonal.container.color` | `color` | `{tanrei.sys.color.secondary-container}` |
| `comp.fab-menu.tonal.container.elevation` | `shadow` | `{tanrei.sys.elevation.level3}` |
| `comp.fab-menu.tonal.icon.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.fab-menu.tonal.state-layer.color` | `color` | `{tanrei.comp.fab-menu.tonal.icon.color}` |
| `comp.fab-menu.tonal.open.container.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.fab-menu.tonal.open.container.elevation` | `shadow` | `{tanrei.sys.elevation.level3}` |
| `comp.fab-menu.tonal.open.icon.color` | `color` | `{tanrei.sys.color.on-primary}` |
| `comp.fab-menu.tonal.open.state-layer.color` | `color` | `{tanrei.comp.fab-menu.tonal.open.icon.color}` |
| `comp.fab-menu.tonal.item.container.color` | `color` | `{tanrei.sys.color.secondary-container}` |
| `comp.fab-menu.tonal.item.container.elevation` | `shadow` | `{tanrei.sys.elevation.level2}` |
| `comp.fab-menu.tonal.item.label.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.fab-menu.tonal.item.icon.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.fab-menu.tonal.item.state-layer.color` | `color` | `{tanrei.comp.fab-menu.tonal.item.label.color}` |
| `comp.fab-menu.surface.container.color` | `color` | `{tanrei.sys.color.surface-container-lowest}` |
| `comp.fab-menu.surface.container.elevation` | `shadow` | `{tanrei.sys.elevation.level3}` |
| `comp.fab-menu.surface.icon.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.fab-menu.surface.state-layer.color` | `color` | `{tanrei.comp.fab-menu.surface.icon.color}` |
| `comp.fab-menu.surface.open.container.color` | `color` | `{tanrei.sys.color.secondary-container}` |
| `comp.fab-menu.surface.open.container.elevation` | `shadow` | `{tanrei.sys.elevation.level3}` |
| `comp.fab-menu.surface.open.icon.color` | `color` | `{tanrei.sys.color.on-secondary-container}` |
| `comp.fab-menu.surface.open.state-layer.color` | `color` | `{tanrei.comp.fab-menu.surface.open.icon.color}` |
| `comp.fab-menu.surface.item.container.color` | `color` | `{tanrei.sys.color.surface-container-lowest}` |
| `comp.fab-menu.surface.item.container.elevation` | `shadow` | `{tanrei.sys.elevation.level2}` |
| `comp.fab-menu.surface.item.label.color` | `color` | `{tanrei.sys.color.on-surface}` |
| `comp.fab-menu.surface.item.icon.color` | `color` | `{tanrei.sys.color.primary}` |
| `comp.fab-menu.surface.item.state-layer.color` | `color` | `{tanrei.comp.fab-menu.surface.item.label.color}` |
| `comp.fab-menu.motion.state-duration` | `duration` | `{tanrei.sys.motion.role.state.default}` |
| `comp.fab-menu.motion.state-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.state}` |
| `comp.fab-menu.motion.feedback-duration` | `duration` | `{tanrei.sys.motion.role.feedback}` |
| `comp.fab-menu.motion.open-duration` | `duration` | `{tanrei.sys.motion.duration.medium1}` |
| `comp.fab-menu.motion.open-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.emphasized-decelerate}` |
| `comp.fab-menu.motion.close-duration` | `duration` | `{tanrei.sys.motion.duration.short3}` |
| `comp.fab-menu.motion.close-easing` | `cubicBezier` | `{tanrei.sys.motion.easing.emphasized-accelerate}` |
| `comp.fab-menu.motion.stagger` | `duration` | `{tanrei.sys.motion.duration.stagger}` |

Per-state tokens, valued by the design system's pattern rules (not written by hand):

- `comp.fab-menu.<variant>.<state>.<property>` for the variants `filled`, `tonal`, `surface`, the states `hovered`, `focused`, `pressed`, `disabled` and the properties `container.color`, `container.elevation`, `icon.color`, `state-layer.color`, `open.container.color`, `open.container.elevation`, `open.icon.color`, `open.state-layer.color`, `item.container.color`, `item.container.elevation`, `item.label.color`, `item.icon.color`, `item.state-layer.color`.
- `comp.fab-menu.<state>.<property>` for the states `hovered`, `focused`, `pressed`, `disabled` and the properties `opacity`.

## Guidelines

### When to use

Use a FAB menu when a screen’s main action comes in a few related forms: a new document, a folder or an upload; a photo or a note. The toggle keeps the screen’s one floating action in reach, and pressing it shows the forms as labelled actions right above it.

When there is only one action, use a [main action button](https://tanrei.chnotchy.com/components/main-action-button/). When the choices are many, or need descriptions, use a [menu](https://tanrei.chnotchy.com/components/menu/) from a button or a sheet. When an action belongs to one item rather than to the whole screen, put a button next to that item.

### Choosing a variant

| Variant | Emphasis | Use for |
| --- | --- | --- |
| Filled (default) | Highest | The screen’s main actions at full strength. |
| Tonal | High | A screen whose content should lead. |
| Surface | Neutral | A FAB menu over content that already has color, such as a map or a photo. |

The variant colors the toggle and the items alike. While open, the toggle takes the variant’s other look, rounds into a circle and shows a close glyph, so open never looks like closed. The size sets only the toggle (40, 48 or 64): the items have one size.

### Opening and closing

The FAB menu is an operated overlay that is not modal, like the menu. People act inside it, so it takes focus and gives it back, but nothing else on the screen is blocked: there is no scrim and no focus trap.

- **Opening.** The toggle asks to open with `toggle`. The items appear above it, end-aligned with it, one after another from the nearest, and focus moves to the first item at the top. Opened with Up Arrow, focus goes to the last item instead, the one next to the toggle.
- **Closing.** Escape, the platform’s back, a press outside the items and the toggle, Tab, or focus leaving all ask to close with `close` and the reason. Activating an item emits its click first, then `close` with the reason `item`. A press on the toggle while open is a toggle, not a press outside.
- **Focus back.** When the items close and focus was among them, focus returns to the toggle. Tab from an item closes them and moves on from the toggle, as it would have from it.
- **Who closes.** On the web, Compose and React Native you set `open` (`expanded`, `visible`) from `toggle` and back to false from `close`. On SwiftUI and Flutter the FAB menu opens and closes itself through its binding or controller.

### Do

- Give the toggle a label that names what its actions have in common, such as “Create”. It keeps that name while open. Its expanded state tells a screen reader that it now closes the list.
- Give every item a short label and an icon. The label is what people read; the icon only helps them find it.
- Keep to two to six items. Order them as people scan them: the first is drawn at the top, and the last sits right above the toggle.
- Leave room at the end of scrolling content, so the toggle never permanently covers the last item.

### Don’t

- Don’t disable an item. Leave an unavailable action out of the list. There is no disabled item.
- Don’t rename the toggle to “Close” while open, or swap its icon yourself. The component does both jobs.
- Don’t place a FAB menu and a main action button of the same size on one screen. A screen has one floating action.
- Don’t use a FAB menu for minor, destructive or settings actions.

## Accessibility

### What the component handles

- The toggle is a button named by the label. It exposes that it opens a menu and whether the menu is open, where the platform has those properties, and keeps its name while open.
- While open, the items are a menu named by the same label, each a menu item named by its own label, with its position and the number of items. The icons are hidden.
- Enter, Space or Down Arrow on the toggle open the menu and move focus to the first item. Up Arrow opens it and moves focus to the last item. Down and Up Arrow move between the items and wrap, and Home and End reach the ends. Enter or Space on an item activates it and closes the menu. Escape closes the menu and returns focus to the toggle.
- The menu is not modal: it does not trap focus, does not make the rest of the screen inert and draws no scrim. Tab from an item closes it.
- The toggle’s hit area reaches the minimum target at every size. Each item is at least a target tall, and its whole container is its hit area.
- The toggle does not grow with the text size, because an icon is not text. The items grow and wrap their labels.
- Where motion is reduced, the toggle changes and the items appear and leave at once.
- Debug builds report a missing toggle label, an item without a label or an icon, and fewer than two or more than six items.

Known platform gaps:

- Web: the web never sends `back`. Escape is its close request.
- Web: the items are placed by script against the toggle, not by CSS anchor positioning as the spec’s web note says, just as the menu is.
- A press outside closes the items everywhere. On the web and Flutter it also reaches what is under it. On SwiftUI, Compose and React Native it does not.
- SwiftUI has no menu or menu item role, and no way to say that the toggle opens a menu or that the menu is open. The items are buttons in a container named by the label, each with its position (“2 of 3”, in English only) as its value.
- SwiftUI presents the items in a popover, which places itself. It draws its own bubble and arrow behind the items, centres the column on the toggle’s corner instead of lining it up with the toggle’s end edge, and clips it in the default system; `list.offset` does not set the distance. The popover is to be replaced by Tanrei’s overlay layer.
- SwiftUI: Tab closes the items and returns focus to the toggle, but focus cannot move on from it in the same keystroke.
- SwiftUI: an item’s accessibility frame takes in its drawn shadow, so neighbouring items’ frames overlap by a few points.
- SwiftUI: an SF Symbol wider than it is tall, such as `folder.badge.plus`, reaches past the item’s icon box.
- SwiftUI: the keys on the toggle and the items, and focus moving in and back, are tested as rules, not yet with a real keyboard.
- Compose has no menu role and no property that says a button opens a menu. The items are buttons in a traversal group named by the label, with their position in their collection info, and the toggle’s expand and collapse actions say whether it opens or closes them.
- Flutter exposes no has-popup on the toggle, and no position in the menu or number of items.
- React Native has no property that says a button opens a menu. The toggle exposes only whether the menu is open.
- React Native draws the items in `TanreiProvider`’s overlay layer, so there must be a `TanreiProvider` above it. Tab closes the items and returns focus to the toggle, and the next Tab moves on from it.
- React Native: the items’ placement, their staggered entrance, focus moving in and out, and the press outside have not yet been checked on a device.

### What you need to do

- Write a label for the toggle and for every item. An icon alone is not a name.
- Put the FAB menu in the element tree where its actions belong. Assistive technology reads the toggle in tree order, not where it is drawn.
- On the web, Compose and React Native, set `open` from `toggle` and to false from `close`. The FAB menu only asks.
- Do not move focus yourself when it closes. The FAB menu returns focus to the toggle.
- Keep the toggle clear of the navigation bar, and leave room above it for the items.
