跳到主要内容
menumenu tanreiexpand_morecontrasttranslate
tanreiexpand_more
开始使用WebReactSwiftUIJetpack ComposeFlutterReact Native预设你自己的设计系统与 AI 代理一起使用 Tanrei品牌设计令牌设计系统与定制Theme Studio颜色字体排印图标形状层级动效交互状态行为布局无障碍文字颜色令牌字阶令牌形状令牌层级令牌动效令牌状态令牌间距与度量令牌组件按钮主操作按钮分段按钮按钮组带菜单的按钮工具栏复制按钮复选框单选按钮开关纸片(Chip)菜单滑块滑块字段日历DateTimePicker字段组单选组复选框组下拉选择框下拉选择字段菜单栏文件上传文件选择器 进阶放置区文本输入框 进阶文本字段纸片字段字段组合框 进阶组合框字段数字输入框 进阶数字字段颜色输入框 进阶颜色字段一次性验证码输入框 进阶一次性验证码字段嵌入式文本输入框 进阶聊天输入栏卡片分隔线模态层 进阶对话框列表列表行底部面板侧边面板轮播代码块表格弹出框步骤折叠面板描述列表图示应用外壳页头树视图时间线图片预览调整手柄可调整大小的框滚动区域徽标进度指示器计量条数字滚动器加载指示器消息条(Snackbar)工具提示键盘快捷键标签(Tag)行内代码提示块(Callout)头像空状态标签组(Tag group)骨架屏文件项色块跑马灯文本标签页标签页视图应用栏导航栏导航抽屉搜索链接分页上一页/下一页链接步骤条面包屑目录链接卡片FAB 菜单状态Tanrei 是如何构建的参与贡献许可与致谢
首页组件菜单

菜单

一小组操作或选项,显示在一个临时表面上,这个表面从请求它的元素处打开。

content_copy以 Markdown 格式查看
类别
选择
令牌
comp.menu
层级
基础

概览

Web
tanrei-menu
schedule
SwiftUI
TanreiMenu
schedule
Jetpack Compose
TanreiMenu
schedule
Flutter
TanreiMenuAnchor
schedule
React Native
Menu
schedule

生成自 docs/status/status.json, specs/menu.spec.json

操作、复选项、单选组和破坏性操作

子菜单:在旁边打开下一级的项

由链接组成的菜单:每一行都通向某个地方,其中一行是当前所在的页面,一行在新标签页中打开

用法

锚点是你自己的按钮、图标按钮或输入框。菜单不绘制锚点:它读取锚点的位置,把自己的表面贴着锚点放置,并在关闭时把焦点交还给锚点。你从锚点打开菜单,并在菜单提出请求时关闭它。

菜单有五种项:item(一个操作)、checkbox-item、radio-item、submenu(在旁边打开下一级菜单的项)和 group(容纳其他几种项)。每个平台接收它们的方式,与它接收任何重复子项的方式相同:在 Web 上是子元素,在 SwiftUI 上是视图构建器中的视图,在 Compose 上是内容 lambda 中的调用,在 Flutter 上是 menuChildren 中的 widget,在 React Native 上是一个由普通对象组成、以 kind 区分种类的 items 数组。复选项的 checked 属于它自己。哪个单选项被选中,则由它所在的组按值决定。

项的键盘快捷键是它的 shortcut 属性,用键盘快捷键组件那套与平台无关的按键名称来写(mod+c)。菜单用键盘快捷键组件把它画在行的末尾:在 Apple 的键盘上是 ⌘C,在其他键盘上是 Ctrl+C;同时把它作为该项的键盘快捷键暴露出来。它并不会让这些按键生效:按键要由你的产品来绑定。trailingText 用于快捷键以外的任何内容,例如数量或“Beta”。Web 会画出这一切,以及当前的外观。SwiftUI、Compose、Flutter 和 React Native 待跟进:在跟上之前,它们没有 shortcut 属性,而是像下面的示例那样把快捷键写在 trailingText 里。它们也没有子菜单、separation、iconSpace 和 markIcon,并且仍然在 16 的方框里用各自的标记来画对勾,不给选中的行加填充,组的标题则从标签开始的位置画起。

带 href 的项是链接项:它是平台自己的链接,画法与操作完全相同,也用同样的按键移到它上面。普通激活会触发该项的 click,关闭菜单,然后打开目的地。target 和 targetLabel 与 Button 的相同。current 标出用户当前所在页面的链接项:它以自己的填充绘制,不画对勾。目前只有 Web 提供链接项;SwiftUI、Compose、Flutter 和 React Native 还没有。

菜单也可以是某个区域的上下文菜单:把 trigger 设为 context,锚点就是那个区域,比如画布、列表或一行。用户在其中的某个位置右键点击、长按,或在有焦点的元素上按 Shift+F10 或 ContextMenu 键,来请求菜单。菜单带着该位置发出 contextRequest,由你在那里打开它。Web 已实现上下文触发方式;SwiftUI、Compose、Flutter 和 React Native 还没有。

<tanrei-button variant="text" id="more" label="更多"><span slot="icon">more_vert</span></tanrei-button>

<!-- anchor 是菜单从其打开的那个元素的 id。 -->
<tanrei-menu anchor="more">
  <!-- shortcut 用与平台无关的按键名称来写:按用户的键盘画成 ⌘C 或 Ctrl+C。 -->
  <tanrei-menu-item label="复制" leading-icon="content_copy" shortcut="mod+c"></tanrei-menu-item>
  <!-- 组的标题是它的 label 属性,与 <optgroup label> 一样。 -->
  <tanrei-menu-group label="视图">
    <tanrei-menu-checkbox-item label="显示网格" checked></tanrei-menu-checkbox-item>
  </tanrei-menu-group>
  <!-- 在组上或在单个项上写 mark-icon="check",选中的项就用一个单纯的对勾来标记。 -->
  <tanrei-menu-group label="排序方式" value="name" mark-icon="check">
    <tanrei-menu-radio-item label="名称" value="name"></tanrei-menu-radio-item>
    <tanrei-menu-radio-item label="修改日期" value="date"></tanrei-menu-radio-item>
  </tanrei-menu-group>
  <!-- 子菜单:它的子项就是下一级。它触发 toggle,从不触发 close。 -->
  <tanrei-submenu label="最近打开" leading-icon="history">
    <tanrei-menu-item label="Report.pdf"></tanrei-menu-item>
    <tanrei-menu-item label="Notes.txt"></tanrei-menu-item>
  </tanrei-submenu>
  <tanrei-menu-item label="删除" leading-icon="delete" destructive></tanrei-menu-item>
  <!-- 带 href 的项是链接:先触发它的 click,再关闭菜单,然后浏览器跟随链接。 -->
  <tanrei-menu-item label="键盘快捷键" href="/help/shortcuts/"></tanrei-menu-item>
  <tanrei-menu-item label="源代码" href="https://example.com/tanrei" target="new" target-label="在新标签页中打开"></tanrei-menu-item>
</tanrei-menu>

<!-- separation="gap" 让这个菜单及其子菜单用空白来分开各组,无论设计系统选的是什么。 -->
<tanrei-menu anchor="more" separation="gap">…</tanrei-menu>

<!-- trigger="context":id 为 anchor 的区域的上下文菜单。用 aria-label 为它命名。 -->
<div id="canvas">…</div>
<tanrei-menu id="canvas-menu" anchor="canvas" trigger="context" aria-label="画布">…</tanrei-menu>

<script type="module">
  import '@tanrei/web/components/menu';
  const menu = document.querySelector('tanrei-menu');
  // 在锚点上的按下属于锚点自己:它不会关闭菜单,所以在这里切换。
  document.querySelector('#more').addEventListener('click', () => (menu.open = !menu.open));
  // close 带有原因:escape、outside、tab、focus-out 或 item。
  menu.addEventListener('close', () => (menu.open = false));
  // click、change 和 toggle 在项或组上触发并冒泡,所以在菜单上放一个监听器就能全部接住。
  menu.querySelector('[label="复制"]').addEventListener('click', copy);
  menu.querySelector('[label="显示网格"]').addEventListener('change', (e) => setGrid(e.detail.checked));
  menu.querySelector('[label="排序方式"]').addEventListener('change', (e) => sortBy(e.detail.value));

  // 在区域中右键点击、长按、按 Shift+F10 或 ContextMenu 键,都会请求上下文菜单。
  // detail 中有位置、所针对的元素和请求方式。打开菜单,或取消该事件,
  // 把请求留给浏览器自己的菜单。
  const canvasMenu = document.querySelector('#canvas-menu');
  canvasMenu.addEventListener('contextrequest', () => (canvasMenu.open = true));
  canvasMenu.addEventListener('close', () => (canvasMenu.open = false));
</script>
import { Button, Menu, MenuCheckboxItem, MenuGroup, MenuItem, MenuRadioItem, Submenu } from '@tanrei/react';

<Button variant="text" id="more" label="更多" onClick={() => setOpen(!open)}><span slot="icon">more_vert</span></Button>
<Menu anchor="more" open={open} onClose={() => setOpen(false)}>
  <MenuItem label="复制" leadingIcon="content_copy" shortcut={['mod+c']} onClick={copy} />
  <MenuGroup label="视图">
    <MenuCheckboxItem label="显示网格" checked={grid} onChange={(e) => setGrid(e.detail.checked)} />
  </MenuGroup>
  <MenuGroup label="排序方式" value={sort} onChange={(e) => setSort(e.detail.value)}>
    <MenuRadioItem label="名称" value="name" />
    <MenuRadioItem label="修改日期" value="date" />
  </MenuGroup>
  <Submenu label="最近打开" leadingIcon="history">
    <MenuItem label="Report.pdf" onClick={() => openFile('Report.pdf')} />
    <MenuItem label="Notes.txt" onClick={() => openFile('Notes.txt')} />
  </Submenu>
  <MenuItem label="删除" leadingIcon="delete" destructive onClick={remove} />
  {/* 链接项。路由取消这次 click,自己前往目的地;菜单仍会关闭。 */}
  <MenuItem label="键盘快捷键" href="/help/shortcuts/" onClick={(e) => { e.preventDefault(); navigate('/help/shortcuts/'); }} />
</Menu>

{/* id 为 canvas 的区域的上下文菜单:收到请求就打开。 */}
<Menu anchor="canvas" trigger="context" aria-label="画布" open={contextOpen} onContextRequest={() => setContextOpen(true)} onClose={() => setContextOpen(false)}>
  <MenuItem label="粘贴" onClick={paste} />
</Menu>
import Tanrei

// 加在锚点上的修饰符,与 .popover 一样。关闭时会向 isPresented 写入 false。
TanreiIconButton("更多", action: { showMenu = true }) { Image(systemName: "ellipsis") }
    .tanreiMenu(isPresented: $showMenu, placement: .bottomStart) {
        TanreiMenuItem("复制", action: copy)
        TanreiMenuSection("视图") {
            TanreiMenuToggle("显示网格", isOn: $showGrid)
        }
        TanreiMenuSection("排序方式", selection: $sort) {
            // 把 .tag(_:) 直接加在 TanreiMenuOption 上。
            TanreiMenuOption("名称").tag("name")
            TanreiMenuOption("修改日期").tag("date")
        }
        TanreiMenuItem("删除", role: .destructive, action: delete)
    }
import io.github.chnotchy.tanrei.components.TanreiCheckboxMenuItem
import io.github.chnotchy.tanrei.components.TanreiIconButton
import io.github.chnotchy.tanrei.components.TanreiMenu
import io.github.chnotchy.tanrei.components.TanreiMenuGroup
import io.github.chnotchy.tanrei.components.TanreiMenuItem
import io.github.chnotchy.tanrei.components.TanreiRadioMenuItem
import io.github.chnotchy.tanrei.primitives.TanreiText

// 与 DropdownMenu 一样:在锚点所在的 Box 中调用菜单。由你来关闭它:onDismissRequest 带有原因。
Box {
    TanreiIconButton(onClick = { expanded = true }, contentDescription = "更多") { TanreiText("more_vert") }
    TanreiMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
        TanreiMenuItem(text = "复制", onClick = ::copy, trailingText = "⌘C", leadingIcon = { TanreiText("content_copy") })
        TanreiMenuGroup(title = "视图") {
            TanreiCheckboxMenuItem(text = "显示网格", checked = showGrid, onCheckedChange = { showGrid = it })
        }
        TanreiMenuGroup(title = "排序方式", selectedValue = sort, onSelectedValueChange = { sort = it }) {
            TanreiRadioMenuItem(text = "名称", value = "name")
            TanreiRadioMenuItem(text = "修改日期", value = "date")
        }
        TanreiMenuItem(text = "删除", onClick = ::delete, destructive = true, leadingIcon = { TanreiText("delete") })
    }
}
import 'package:tanrei/tanrei.dart';

// 与 MenuAnchor 一样:由控制器打开它。菜单请求关闭时,会带着原因调用 onClose,并自行关闭。
final TanreiMenuController menu = TanreiMenuController();

TanreiMenuAnchor(
  controller: menu,
  onClose: (TanreiMenuCloseReason reason) {},
  menuChildren: <Widget>[
    TanreiMenuItem(
      onPressed: copy,
      leadingIcon: const Icon(AppSymbols.contentCopy),
      trailingText: '⌘C',
      child: const Text('复制'),
    ),
    TanreiMenuGroup<String>(
      title: '视图',
      children: <Widget>[
        TanreiCheckboxMenuItem(
          value: showGrid,
          onChanged: (bool value) => setState(() => showGrid = value),
          child: const Text('显示网格'),
        ),
      ],
    ),
    TanreiMenuGroup<String>(
      title: '排序方式',
      groupValue: sort,
      onChanged: (String value) => setState(() => sort = value),
      children: const <Widget>[
        TanreiRadioMenuItem<String>(value: 'name', child: Text('名称')),
        TanreiRadioMenuItem<String>(value: 'date', child: Text('修改日期')),
      ],
    ),
    // onPressed 为 null 时,这一项被禁用。
    TanreiMenuItem(onPressed: delete, destructive: true, child: const Text('删除')),
  ],
  builder: (BuildContext context, TanreiMenuController controller, Widget? child) =>
      TanreiIconButton(onPressed: controller.open, icon: const Icon(AppSymbols.moreVert), semanticLabel: '更多'),
);
import { useRef, useState } from 'react';
import type { View } from 'react-native';
import { IconButton, Menu } from '@tanrei/react-native';

const more = useRef<View>(null);
const [open, setOpen] = useState(false);

// 告诉辅助技术锚点的菜单是否打开:在这里菜单无法替你设置。
<IconButton ref={more} accessibilityLabel="更多" accessibilityState={{ expanded: open }} onPress={() => setOpen(true)}>
  more_vert
</IconButton>
// 受控:在 onDismiss 中把 visible 设为 false,onDismiss 会收到原因。
<Menu
  visible={open}
  anchorRef={more}
  accessibilityLabel="更多"
  onDismiss={() => setOpen(false)}
  items={[
    { label: '复制', leadingIcon: 'content_copy', trailingText: '⌘C', onPress: copy },
    { kind: 'group', title: '视图', items: [{ kind: 'checkbox-item', label: '显示网格', checked: grid, onCheckedChange: setGrid }] },
    { kind: 'group', title: '排序方式', value: sort, onValueChange: setSort, items: [
      { kind: 'radio-item', label: '名称', value: 'name' },
      { kind: 'radio-item', label: '修改日期', value: 'date' },
    ] },
    { label: '删除', leadingIcon: 'delete', destructive: true, onPress: remove },
  ]}
/>

规格

辅助文本、后置图标和禁用的项

位于对勾列和图标列上方的组标题

标记列和图标列:iconSpace 的 auto(默认)和 never

标记:默认系统的方框和圆环,以及用 mark-icon 指定的单纯对勾

分隔方式:组之间的分隔线,以及间隙

在默认系统中,复选项被标记成复选框的样子,单选项被标记成单选按钮的样子,无论该项是否选中都会画出来:空的方框和带对勾的填充方框,细的圆环和带填充的粗圆环。这样,处于关闭状态的选项仍然看得出是一个选项。这些只是标记列中的图形,不是控件:可以按下和聚焦的只有行本身。方框中的对勾以控件自身标记所用的较粗字重绘制,与复选框的对勾一样。

markIcon 可以写在单个项上,也可以写在组上以作用于组内所有项,它指定图标集中的一个图标,改用它来标记选中的项。check 会画一个单纯的对勾,大小与旁边的前置图标相同(默认系统中是 24),项未选中时什么也不画。设计系统自行选择标记:m3-like 和 cupertino 使用单纯的对勾。对勾在从右到左的布局中不镜像。菜单打开期间某一项的选中状态改变时,标记在原地变化,不发生移动;在减弱动态效果时则立即变化。

选中的行靠它的标记来辨认。在默认系统中,标准对比度下选中的行不加填充,所以静止的菜单里唯一有填充的行就是用户当前所在页面的那一行。在用户要求更强提示的中对比度和高对比度下,选中的行也会加上填充:这个填充是覆盖在菜单表面之上的一层,用的是这一行自身的内容颜色,在浅色和深色主题中都比悬停的行强一档,所以在抬升的表面上,选中的行、悬停的行和静止的行可以相互区分。菜单从它所处配色方案的令牌中读取填充;按对比度方案决定填充的是设计系统。子菜单打开期间,打开它的那一项保持悬停填充,以此标出通往当前所用那一级的路径,而且它绝不会看起来像有填充的行。

组的标题从行内容开始的位置画起:菜单画出对勾列或前置图标列时,标题位于这些列的上方;两者都不画时,标题位于标签的上方。它在距另一侧边缘相同距离处结束,会换行,会随文本变大而增高,在从右到左的布局中会镜像。

构成

构成

绘制在 Web 包所绘的测试样例 actions 之上。
部件 说明
1 surface The floating sheet that holds the items: its color, corners, elevation and padding. It is placed against the anchor, or at the point a context menu was asked for, and drawn above everything else.
2 container One item's row. It carries hover, focus and press, the fill of a checked item, and the fill of a submenu item whose submenu is open.
3 mark · 可选 What shows whether a checkbox or radio item is chosen, in the mark column at the start of the row. By default it is the look of the matching control, drawn as a mark: a box for a checkbox item (empty, or filled with the icon set's check) and a ring for a radio item (thin, or heavy with its fill), in both conditions. Where the design system chooses the check (comp.menu.checkbox-item.mark, comp.menu.radio-item.mark) or the item or its group names a glyph (markIcon), it is that glyph of the system's icon set while the item is checked and nothing while it is not. It is never a control of its own: it takes no focus and no press, and the item stays a menu item checkbox or a menu item radio. Its column is kept, empty, in the other rows of its section while iconSpace is auto, in every row while it is always, and in its own row only while it is never (MNU-59).
4 leading-icon · 可选 An icon before the label. Its column is kept, empty, in the other rows of its section while iconSpace is auto, in every row while it is always, and in its own row only while it is never (MNU-59).
5 label The item's text and its accessible name.
6 supporting-text · 可选 A line below the label that says more about the item.
7 trailing-text · 可选 Text at the end of the row that is not a keyboard shortcut, such as a count or "Beta". It shares the trailing column with the shortcut.
8 shortcut · 可选 The item's keyboard shortcut at the end of the row, in the trailing column: the keyboard shortcut component at its small size, in the form comp.menu.shortcut.variant names, its legends in the trailing text's style and color (MNU-31).
9 trailing-icon · 可选 An icon at the end of the row. A submenu item draws its submenu indicator in this column instead, and a link item that opens a new context draws its mark there, the glyph open_in_new, while the product names no glyph for it (MNU-55).
10 submenu-indicator · 可选 The glyph chevron_right of the system's icon set at the end of a submenu item's row, in the trailing icon column: it says the item opens another level, and points to the side that level opens on. The icon set marks it as mirrored, so the icon helper turns it in a right-to-left layout. The menu draws no chevron of its own.
11 submenu · 可选 The surface a submenu item opens: a menu of its own beside the surface that holds the item, drawn with the surface's tokens and holding items of every kind, further submenus included.
12 group · 可选 A run of related items (a group item), optionally under a title. The menu separates a group from what is next to it by a divider, or under gap by drawing it as a tile of its own, as its separation names, and a group of radio items is one set of choices.
13 group-title · 可选 The heading drawn above a group's items when the group has a title. It starts where the rows' content starts, not after the check and icon columns. It names the group and takes no focus.
14 divider · 可选 The hairline the menu draws between two groups while its separation is divider. Under gap there is none: each group is a tile of its own, and the space between the tiles parts them (MNU-36).
15 focus-ring Gap-then-ring indicator shown for keyboard focus, drawn inside the item's row.

尺寸

尺寸

长度 令牌 值(px)
surface padding-inline tanrei.comp.menu.surface.padding-inline 4
绘制在 Web 包所绘的测试样例 actions 之上。

属性

属性 类型 默认值 说明
open boolean false Whether the menu is shown. The product opens it from its anchor (a button, an icon-only Button, a field), or on contextRequest while trigger is context. Where open is a plain value (web, Compose, React Native) the product closes the menu by setting it to false when the menu emits close; where it is a binding or a controller (SwiftUI, Flutter) the menu closes itself through it (MNU-5).
placement bottom-start | bottom-end | top-start | top-end "bottom-start" Where the surface goes against the anchor: below or above it, with its start or end edge aligned to the anchor's. The menu flips to the other side when this side has no room. For a context menu opened at a point it is where the surface goes from the point: bottom-start puts its block-start, inline-start corner there (MNU-61).
trigger anchor | context "anchor" What the menu opens from. anchor: the product opens it from its anchor — a button, an icon-only Button, a field — and the surface is placed against the anchor's edges. context: the menu is the context menu of an area, and the anchor is that area (a canvas, a list, a row, a document's body of text); a person asks for the menu at a point in it — a secondary click, a long press of a touch or a pen, Shift+F10 or the ContextMenu key on the element of the area that has focus — and the menu emits contextRequest, the product sets open, and the surface opens at that point, or against that element for the keyboard. The items, the keys, the submenus and the close reasons are the same in both (MNU-60 to MNU-63).
separation auto | divider | gap "auto" How a group is told from what is next to it. divider: a line between them. gap: empty space and no line. Auto takes comp.menu.separation, the design system's answer (divider in the default system). It applies to the menu and to every submenu of it.
iconSpace auto | always | never "auto" Whether a row keeps the room of the mark column and of the leading icon column where it has nothing to draw there, so that the labels of a section start on one line — a section being a group, or a run of adjacent items outside any group. auto: a row keeps a column's room while another item of its section draws in that column (a checkbox or radio item its mark, an item its leading icon), so that a section without marks or icons draws its labels where the row's content starts. always: every row keeps both. never: only a row that draws in a column has it. The tree view's and the navigation drawer's iconSpace, with the same values and default. It applies to the menu and to every submenu of it (MNU-59).
density regular | compact "regular" How much room a row takes: its least height, its paddings, the gap between its columns and the size of its glyphs (the mark, the leading and the trailing icon), and never its text style. regular (a 48 row with 24 glyphs in the default system) is the menu of a product used by touch as well as by pointer. compact (a 32 row with 20 glyphs, the tree view's compact row) is for a tool used with a pointer, whose menus are long and opened often. For touch a row is never lower than the touch target in either (MNU-12). It applies to the menu and to every submenu of it (MNU-64). A design system may add values.

事件

事件 说明
close The menu asks to be closed, every open submenu with it: Escape or the platform's back while no submenu is open, a press outside, Tab, focus leaving it, an item's activation at any level, or its anchor no longer drawn (MNU-65). Carries which of these it was, named escape, back, outside, tab, focus-out, item or anchor-hidden (in the platform's casing). Closing a submenu alone is the submenu item's toggle, not this event.
contextRequest While trigger is context: the person asked for the context menu in the anchor's area. Carries the point in the viewport's coordinates, the element it was asked about (the one under the pointer, or the one with focus) and how it was asked: pointer (a secondary click, or the context-menu command of assistive technology), long-press (a touch or a pen held still) or keyboard (Shift+F10, the ContextMenu key). Set open to true to show the menu there. Cancelling it leaves the request to the platform: its own context menu answers it (MNU-60).

条目

该组件重复排列的条目种类。测试样例按顺序列出条目;各平台以各自的惯用方式为每个种类命名。

item

An action: it does something and closes the menu. With an href it is a link: it goes somewhere, and the menu closes.

属性 类型 默认值 说明
label string "" The item's text and its accessible name. Required.
supportingText string "" A line below the label that says more about the item.
leadingIcon string "" The name of an icon (a glyph of the system's icon set) before the label.
trailingIcon string "" The name of an icon at the end of the row.
trailingText string "" Text at the end of the row that is not a keyboard shortcut, such as a count or "Beta". An item's shortcut goes in shortcut; an item that has both draws the shortcut.
shortcut strings The item's keyboard shortcut, in the keyboard shortcut component's key names: one chord per string, pressed in order (["mod+c"]). The menu draws it at the end of the row for the keyboard the person uses and exposes it as the item's keyboard shortcut (MNU-24, MNU-31). It does not make the keys work; the product binds them.
destructive boolean false Marks an action that deletes or discards something; it is drawn in the error color.
disabled boolean false Prevents activation. The item stays reachable with the arrow keys.
href string "" The destination: an address as the platform's links take one (a URL; on the web a path or a fragment as well). Not empty, it makes the item a link item (a control with a destination is a link): the platform's own link, exposed as a menu item, drawn exactly as an action is and moved to with the same keys; a plain activation emits click, closes the menu and opens the destination (MNU-53, MNU-54). Empty, the item is an action and the product acts on click. Only the item kind has one: a checkbox item, a radio item and a submenu item take no destination (MNU-57).
target same | new "same" Where the destination opens, as the Button's target. same: where the person is. new: a new context — a new tab or window on the web, outside the current screen elsewhere — which the item marks with the glyph open_in_new in its trailing icon column, unless trailingIcon names one, and says through targetLabel (MNU-55). Ignored without href.
targetLabel string "" What assistive technology reads after the link item's name while target is new ("opens in a new tab"), as the Button's targetLabel: Tanrei ships no words of its own. Required while target is new; ignored otherwise.
current boolean false Marks a link item as the destination the person is at now (the current page): exposed as the current item and drawn with the fill of the row the person is on, comp.menu.current.container.color, in every contrast scheme, with no mark and no change of weight; it stays a link (MNU-58). The product sets it; the menu never does. Ignored without href.
事件 说明
click The item was activated by pointer, touch, keyboard or assistive technology. A link item emits it before the menu closes and before its destination opens; a product that takes the navigation itself (a client-side router) cancels it, and the menu still closes (MNU-54).
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-menu-item TanreiMenuItem TanreiMenuItem TanreiMenuItem MenuItem
label · 属性 label title text child label
supportingText · 属性 supportingText supportingText supportingText supportingText supportingText
leadingIcon · 属性 leadingIcon leadingIcon leadingIcon leadingIcon leadingIcon
trailingIcon · 属性 trailingIcon trailingIcon trailingIcon trailingIcon trailingIcon
trailingText · 属性 trailingText trailingText trailingText trailingText trailingText
shortcut · 属性 shortcut shortcut shortcut shortcut shortcut
destructive · 属性 destructive role destructive destructive destructive
disabled · 属性 disabled disabled enabled onPressed disabled
href · 属性 href destination href href href
target · 属性 target target target target target
targetLabel · 属性 targetLabel targetLabel targetLabel targetSemanticLabel targetLabel
current · 属性 current isCurrent current isCurrent current
click · 事件 click action onClick onPressed onPress
备注 With href the item's shadow root holds an <a role="menuitem" tabindex="-1"> in place of its row element, drawn through the shared link primitive (packages/web/src/primitives/link.ts) as <tanrei-list-item>'s is, with Space left among the keys that press it (MNU-54). target is the target attribute with the values same and new (the HTML spellings _self and _blank are read as those two), targetLabel the target-label attribute, and rel (to which new adds noopener) and download are web-only and forwarded to the anchor. click is the anchor's own click: cancelling it (preventDefault()) cancels the navigation and not the menu's close. The menu fires close from that click only when event.button is 0 and no modifier key is down; Enter with a modifier key is passed to the anchor, and Space calls the anchor's click(). A disabled link item is the anchor without href, with aria-disabled="true". An href attribute on tanrei-menu-checkbox-item, tanrei-menu-radio-item or tanrei-submenu is ignored and reported (MNU-57). TanreiMenuItem(_ title:, role:, action:), as Button(role:); role: .destructive is destructive. destination: URL? = nil makes it a SwiftUI Link (the link trait in place of the button's), opened by the environment's openURL after action and after isPresented is written, which an app that routes addresses itself answers with its own OpenURLAction; target: TanreiLinkTarget = .same and targetLabel (the accessibility hint), as the Button's. href: String? = null makes it a link: opened through LocalUriHandler.current after onClick and onDismissRequest (an app that routes addresses itself provides its own UriHandler); Compose has no link role for a clickable element, so the node drops Role.Button and carries no role; target: TanreiLinkTarget = TanreiLinkTarget.Same, and targetLabel is appended to the state description, as the Button's. As MenuItemButton: a null onPressed disables it. href: Uri? = null makes it a link (Semantics(link: true) with the address as its linkUrl), opened by the nearest TanreiLinkOpener after onPressed and after the menu has closed; target is a TanreiLinkTarget and targetSemanticLabel the semantics hint, as the Button's. href?: string on an entry whose kind is 'item' makes it a link (accessibilityRole="link"): onPress receives an event whose preventDefault() keeps the menu from calling Linking.openURL(href); target is "same" | "new" and targetLabel the accessibilityHint, as the Button's. An href on an entry of another kind is ignored and reported (MNU-57).

checkbox-item

An option that is switched on or off on its own, such as "Show grid". It is marked as a checkbox unless the design system, its group or the item says otherwise.

属性 类型 默认值 说明
label string "" The item's text and its accessible name. Required.
supportingText string "" A line below the label that says more about the item.
leadingIcon string "" The name of an icon (a glyph of the system's icon set) before the label.
trailingIcon string "" The name of an icon at the end of the row.
trailingText string "" Text at the end of the row that is not a keyboard shortcut, such as a count or "Beta". An item's shortcut goes in shortcut; an item that has both draws the shortcut.
shortcut strings The item's keyboard shortcut, in the keyboard shortcut component's key names: one chord per string, pressed in order (["mod+c"]). The menu draws it at the end of the row for the keyboard the person uses and exposes it as the item's keyboard shortcut (MNU-24, MNU-31). It does not make the keys work; the product binds them.
checked boolean false Whether the option is on. It is the item's own: the menu switches it when the item is activated and emits the item's change.
markIcon string "" The name of a glyph of the system's icon set to mark this item with while it is checked, instead of the mark the design system chooses ("check" draws a plain check). Empty takes the group's markIcon, and then the design system's choice (MNU-38).
disabled boolean false Prevents activation. The item stays reachable with the arrow keys.
事件 说明
change The item was activated; carries its new checked.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-menu-checkbox-item TanreiMenuToggle TanreiCheckboxMenuItem TanreiCheckboxMenuItem MenuCheckboxItem
label · 属性 label title text child label
supportingText · 属性 supportingText supportingText supportingText supportingText supportingText
leadingIcon · 属性 leadingIcon leadingIcon leadingIcon leadingIcon leadingIcon
trailingIcon · 属性 trailingIcon trailingIcon trailingIcon trailingIcon trailingIcon
trailingText · 属性 trailingText trailingText trailingText trailingText trailingText
shortcut · 属性 shortcut shortcut shortcut shortcut shortcut
checked · 属性 checked isOn checked value checked
markIcon · 属性 markIcon markIcon markIcon markIcon markIcon
disabled · 属性 disabled disabled enabled onChanged disabled
change · 事件 change isOn onCheckedChange onChanged onCheckedChange
备注 TanreiMenuToggle(_ title:, isOn:), as Toggle in a menu; writing the binding is change. As CheckboxMenuButton: value is checked, a null onChanged disables it.

radio-item

One choice of several in its group, such as "Sort by name"; its group holds which one is checked. It is marked as a radio button unless the design system, its group or the item says otherwise.

属性 类型 默认值 说明
label string "" The item's text and its accessible name. Required.
supportingText string "" A line below the label that says more about the item.
leadingIcon string "" The name of an icon (a glyph of the system's icon set) before the label.
trailingIcon string "" The name of an icon at the end of the row.
trailingText string "" Text at the end of the row that is not a keyboard shortcut, such as a count or "Beta". An item's shortcut goes in shortcut; an item that has both draws the shortcut.
shortcut strings The item's keyboard shortcut, in the keyboard shortcut component's key names: one chord per string, pressed in order (["mod+c"]). The menu draws it at the end of the row for the keyboard the person uses and exposes it as the item's keyboard shortcut (MNU-24, MNU-31). It does not make the keys work; the product binds them.
value string "" Identifies the choice: the item is checked while its group's value equals it. Unique within its group; required.
markIcon string "" The name of a glyph of the system's icon set to mark this item with while it is checked, instead of the mark the design system chooses ("check" draws a plain check). Empty takes the group's markIcon, and then the design system's choice (MNU-38).
disabled boolean false Prevents activation. The item stays reachable with the arrow keys.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-menu-radio-item TanreiMenuOption TanreiRadioMenuItem TanreiRadioMenuItem MenuRadioItem
label · 属性 label title text child label
supportingText · 属性 supportingText supportingText supportingText supportingText supportingText
leadingIcon · 属性 leadingIcon leadingIcon leadingIcon leadingIcon leadingIcon
trailingIcon · 属性 trailingIcon trailingIcon trailingIcon trailingIcon trailingIcon
trailingText · 属性 trailingText trailingText trailingText trailingText trailingText
shortcut · 属性 shortcut shortcut shortcut shortcut shortcut
value · 属性 value tag value value value
markIcon · 属性 markIcon markIcon markIcon markIcon markIcon
disabled · 属性 disabled disabled enabled enabled disabled
备注 TanreiMenuOption(_ title:).tag(value) inside a section with a selection, as a Picker's tagged options. As RadioMenuButton under a RadioGroup: the item takes its value and enabled, the group the selection.

submenu · 分组 — 包含条目

An item that opens another level of the menu beside it: it holds that level's items, of any kind, further submenus included. It is not an action and has no shortcut.

属性 类型 默认值 说明
label string "" The item's text and its accessible name. Required.
supportingText string "" A line below the label that says more about the item.
leadingIcon string "" The name of an icon (a glyph of the system's icon set) before the label.
trailingText string "" Text at the end of the row, before the submenu indicator: the current value of what the submenu sets ("English"), or a count.
open boolean false Whether the item's submenu is shown. It is the item's own: the menu sets it when the submenu is opened or closed and emits the item's toggle; the product may also set it. It shows nothing while the surface that holds the item is not shown.
disabled boolean false Prevents the submenu from opening. The item stays reachable with the arrow keys.
事件 说明
toggle The item's submenu was opened or closed by the person; carries its new open.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-submenu TanreiSubmenu TanreiSubmenu TanreiSubmenu MenuSubmenu
label · 属性 label title text child label
supportingText · 属性 supportingText supportingText supportingText supportingText supportingText
leadingIcon · 属性 leadingIcon leadingIcon leadingIcon leadingIcon leadingIcon
trailingText · 属性 trailingText trailingText trailingText trailingText trailingText
open · 属性 open isExpanded expanded controller open
disabled · 属性 disabled disabled enabled enabled disabled
toggle · 事件 toggle isExpanded onExpandedChange onOpenChanged onOpenChange
备注 <tanrei-submenu label="Open recent"> holds the submenu's items as its children, as tanrei-menu holds its own; it draws the row in its parent's surface and its own surface as a manual popover in the top layer. open reflects, and toggle fires on it and bubbles. TanreiSubmenu(_ title:, isExpanded:) { … }, as a Menu nested in a Menu; the binding is optional, and writing it is toggle. disabled follows .disabled(_:). TanreiSubmenu(text, expanded, onExpandedChange) { … }: the caller keeps expanded (state hoisting), and an overload without the pair keeps it itself. enabled is the inverse flag. As SubmenuButton: TanreiSubmenu(child:, menuChildren: […], controller:, onOpenChanged:); the optional MenuController opens and closes it, as the anchor's does the menu. { kind: 'submenu', label, items: […], open?, onOpenChange? }: uncontrolled unless open is given.

group · 分组 — 包含条目

A run of related items, optionally titled. The menu separates a group from what is next to it, by a divider or a gap, and a group of radio items is one set of choices.

属性 类型 默认值 说明
title string "" A short heading drawn above the group's items, which also names the group; empty draws none and leaves the group unnamed.
value string null The value of the group's checked radio item; null checks none. The group sets it when a radio item is chosen and emits its change; the product may also set it. Unused by a group without radio items.
markIcon string "" The name of a glyph of the system's icon set that marks the group's checked checkbox and radio items, for those that name none of their own ("check" draws a plain check). Empty takes the design system's choice (MNU-38).
事件 说明
change A radio item of the group was chosen; carries the group's new value.
规格 Web SwiftUI Jetpack Compose Flutter React Native
条目 tanrei-menu-group TanreiMenuSection TanreiMenuGroup TanreiMenuGroup MenuGroup
title · 属性 label title title title title
value · 属性 value selection selectedValue groupValue value
markIcon · 属性 markIcon markIcon markIcon markIcon markIcon
change · 事件 change selection onSelectedValueChange onChanged onValueChange
备注 The title is the label attribute, as <optgroup label>: a title attribute would also draw the browser's tooltip. The group sets its own value before firing change. TanreiMenuSection(_ title:) { … } as Section; TanreiMenuSection(_ title:, selection:) { TanreiMenuOption … } for radio items, as an inline Picker. Writing the binding is change. TanreiMenuGroup(title, selectedValue, onSelectedValueChange) { TanreiRadioMenuItem(text, value) }; the caller keeps selectedValue. TanreiMenuGroup<T>(title:, groupValue:, onChanged:, children:), which is also the RadioGroup of its radio items. { kind: 'group', title?, value?, onValueChange?, items: (MenuItem | MenuCheckboxItem | MenuRadioItem)[] }.

状态

行为

以 EARS 形式写成的需求。每个平台的实现都按这些 ID 逐条测试。需求原文取自规格文件,原样以英文呈现。

ID 需求
MNU-1 WHEN open becomes true THE menu SHALL present its surface in the platform's overlay layer (the web's top layer through the popover API, SwiftUI's popover, Compose's Popup, Flutter's Overlay, React Native's overlay layer that TanreiProvider renders), above other content at comp.menu.surface.elevation, and SHALL move focus to its first enabled item, wherever that item stands in the surface: inside a group as well, a menu whose every item is in a group included, and a menu the product mounts with open true included; WHEN open becomes false THE menu SHALL remove the surface and, if focus was inside it, return focus to the anchor (a context menu: to the element MNU-62 names). A menu is an operated overlay, so it takes focus and returns it although it is not modal.
MNU-2 WHILE trigger is anchor THE menu SHALL place its surface comp.menu.surface.offset from the anchor on the side and edge that placement names (bottom-start: below the anchor with the start edges aligned; bottom-end: below with the end edges aligned; top-start and top-end: above); WHERE that side has less room than the surface needs within comp.menu.surface.viewport-margin of the edge of the visible part of the window, above the on-screen keyboard while one is shown, and the other side has more THE menu SHALL flip to the other side; it SHALL shift along the anchor to stay within that margin, SHALL limit its height to the least of the height its items need, the most height the product's own style gives the surface where it gives one (on the web, max-block-size on the surface part), and the room left, place the surface by that height and scroll its items beyond it, and SHALL keep following the anchor while it moves. WHEN the on-screen keyboard shows or hides, or the visible part of the window otherwise moves or changes size, THE menu SHALL place its surface again inside what is visible. WHEN the layout direction is right-to-left start and end SHALL mirror.
MNU-3 THE menu SHALL make its surface at least comp.menu.surface.min-width and at most comp.menu.surface.max-width wide — as wide as its widest item within those bounds — inset its items by comp.menu.surface.padding-block and comp.menu.surface.padding-inline, fill it with comp.menu.surface.color, round it by comp.menu.surface.shape eased by sys.shape.corner-smoothing, and clip its items to that shape.
MNU-4 WHEN the menu opens THE menu SHALL fade its surface in and grow it from the edge that faces the anchor over comp.menu.motion.open-duration with comp.menu.motion.open-easing, and WHEN it closes fade it out over comp.menu.motion.close-duration with comp.menu.motion.close-easing; WHERE motion is reduced it SHALL appear and disappear at once. WHEN it is mounted with open true THE menu SHALL draw its surface, and the surface of every submenu open in it (MNU-50), without that motion while the document has not yet been first painted, and with it when it is mounted after that.
MNU-5 WHEN the user presses Escape or the platform's back while no submenu is open, presses outside every surface of the menu and the anchor (the area of a context menu is outside, MNU-62), presses Tab or Shift+Tab at any level, or moves focus out of the menu and all of its submenus THE menu SHALL emit close once with that reason and SHALL be closed, every open submenu with it (MNU-49): 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 anchor as they would have from it (a context menu: from the element MNU-62 names). Whether a press outside also reaches the element under it SHALL follow the platform and be stated in its README.
MNU-6 WHEN the user activates an enabled item (an action) at any level by pointer, touch, Enter or Space THE menu SHALL emit the item's click once, then emit close and be closed as MNU-5 says, every open submenu with it, an item that is a link (MNU-53) opening its destination after that, and only for an activation that MNU-54 counts; WHEN the user activates an enabled checkbox item THE menu SHALL switch its checked and emit the item's change once with the new checked; WHEN the user activates an enabled radio item whose value is not its group's value THE menu SHALL set the group's value to the radio item's value, so that the group's other radio items become unchecked, and emit the group's change once; activating a radio item that is already checked SHALL emit no change. After a checkbox or radio item, a checked radio item included, the menu SHALL close as for an action when the activation came from pointer, touch or Enter, and SHALL stay open, at every level, when it came from Space. Activating a submenu item opens its submenu and closes nothing (MNU-43).
MNU-7 WHILE the menu is open THE menu SHALL move focus among the items of the surface that holds focus — the menu's own or one submenu's, never across levels — to the next and previous item with Down Arrow and Up Arrow, wrapping at the ends and passing through groups as if their items were the surface's, to the first and last item with Home and End, and to the next item of that surface whose label begins with the typed characters when printable characters are typed within one second of each other; disabled items SHALL be reachable but SHALL NOT activate, a submenu item SHALL take focus as any other item, and dividers, groups and group titles SHALL NOT take focus.
MNU-8 WHEN a pointer moves onto an item THE menu SHALL move focus to that item without showing the focus ring and render it hovered, so that the arrow keys continue from it, except while the pointer is on its way to an open submenu (MNU-45); WHEN the pointer leaves the surface focus SHALL stay on that item.
MNU-9 WHILE an item's disabled is true THE menu SHALL NOT emit its click or change, nor its group's change, for it and SHALL expose the disabled state to assistive technology.
MNU-10 WHILE an item is hovered, keyboard-focused or pressed THE menu SHALL render its container, label, supporting text, trailing text, icons, mark (a glyph's mark color, and a control look's control and control-mark colors) and opacity from the component tokens of its condition in the highest-precedence active state, with precedence pressed over focused over hovered; focus gained by pointer or touch SHALL NOT select the focused state.
MNU-11 WHEN an item receives keyboard focus THE menu SHALL show the focus ring inside the item's container, which the surface would otherwise clip, without animation; focus gained by pointer or touch SHALL NOT show it.
MNU-12 WHERE an item is shorter than the minimum target THE menu SHALL make its container at least sys.target.touch tall for touch input, in both densities, because stacked items cannot extend a hit area over each other, and for fine pointers at least sys.target.pointer tall WHILE density is regular and as tall as its density gives WHILE density is compact (MNU-64), the one place where a row of the menu is lower than the pointer target; a platform that cannot tell the input type SHALL use sys.target.touch. The whole row SHALL be the item's hit area.
MNU-13 WHILE an item's disabled is true THE menu SHALL render its container, label, supporting text, trailing text, icons, mark (a glyph's mark color, and a control look's control and control-mark colors) and opacity from the disabled component tokens of its condition.
MNU-14 WHEN placement names a value the menu does not define THE menu SHALL render with the default value, and in debug builds report the unknown value.
MNU-15 WHEN the menu item is pressed by pointer, touch, Enter or Space THE menu item 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.
MNU-16 WHILE pressed THE menu item 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; label, supporting text, trailing text, icons, mark, 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.menu.motion.feedback-duration with comp.menu.motion.state-easing, fades over comp.menu.motion.state-duration with comp.menu.motion.state-easing once the press has ended and press-hold has passed, and appears whole where motion is reduced.
MNU-17 THE menu item 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.
MNU-18 THE menu SHALL draw an item from the condition it is in: destructive for an item (an action) whose destructive is true, selected for a checkbox item whose checked is true and for a radio item whose value equals its group's value, current for a link item whose current is true (MNU-58), expanded for a submenu item whose submenu is open, otherwise unselected.
MNU-19 THE menu SHALL lay out an item as one row: comp.menu.container.padding-inline.regular from each edge, the mark column and the leading icon column, each where MNU-59 keeps it, the label with the supporting text below it taking the remaining width, then the trailing column, which holds the shortcut or the trailing text, and the trailing icon column, which holds the trailing icon or, in a submenu item, the submenu indicator (MNU-42) or, in a link item that opens a new context and names no trailing icon, its mark (MNU-55), each comp.menu.container.gap.regular from the next; an empty trailing icon column SHALL keep its width in every item of the same surface so that shortcuts, trailing text and trailing icons align, each surface — the menu's and each submenu's — reserving its own, and WHEN the layout direction is right-to-left THE menu SHALL mirror the row.
MNU-20 THE menu SHALL make an item at least comp.menu.container.height.regular tall, pad its content by comp.menu.container.padding-block.regular above and below, draw the label in comp.menu.label.typescale, the supporting text in comp.menu.supporting-text.typescale and the trailing text and the shortcut's legends in comp.menu.trailing-text.typescale, and grow with its content and with enlarged text rather than clip or truncate it.
MNU-21 THE menu SHALL draw leadingIcon at comp.menu.leading-icon.size.regular and trailingIcon at comp.menu.trailing-icon.size.regular through the icon helper, with the family and fill of the item's condition (selected for a checked item and for a current link item, otherwise unselected), sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max; an empty name SHALL draw nothing.
MNU-22 WHILE a checkbox or radio item is checked and its mark is a glyph (MNU-38) THE menu SHALL draw that glyph of the system's icon set — check where the design system's choice is check, the named glyph where markIcon names one — in its mark column, centered in a box of side comp.menu.mark.size.regular, in the mark color of its condition, with sys.icon.family.selected, sys.icon.fill.selected, sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, and SHALL draw nothing in that column while the item is not checked; it SHALL NOT mirror the check when the layout direction is right-to-left, a glyph that markIcon names following MNU-33; THE menu SHALL draw no mark of its own (no path, shape or character of its own for the check). WHEN checked changes while the menu is open THE menu SHALL fade the glyph in or out over comp.menu.motion.state-duration with comp.menu.motion.state-easing without moving or scaling it, and at once where motion is reduced.
MNU-23 WHILE the menu's separation is divider (MNU-35) THE menu SHALL draw a line of comp.menu.divider.thickness in comp.menu.divider.color across the surface's inner width, inside comp.menu.surface.padding-inline on each side (the same inset as the rows, not the rows' own padding) between a group and an adjacent group or an adjacent item outside any group, with comp.menu.divider.space above and below it, and SHALL expose it as a separator that takes no focus; it SHALL draw no line before the first or after the last element of the surface.
MNU-24 THE menu SHALL expose the menu role, named by its anchor (a context menu as MNU-63 says); each item as a menu item, a link item included (MNU-53), each checkbox item as a menu item checkbox and each radio item as a menu item radio with its checked state, whatever its mark looks like, each with its position and the size of the surface it is in, named by label and described by supporting text, a link item that opens a new context also by its targetLabel (MNU-55), and a link item that is current with the current state (MNU-58); each submenu item as a menu item that opens a menu, with whether that menu is open (aria-haspopup with the value menu, and aria-expanded, on the web), described also by its trailing text, and each submenu's surface as a menu of its own named by its submenu item's label; shortcut SHALL be exposed as the item's keyboard shortcut in the form KBS-12 gives (aria-keyshortcuts on the web) where the platform has that property, and otherwise the text KBS-10 gives as the item's description; trailing text SHALL be exposed as the item's description only, never as a keyboard shortcut, whatever it holds; icons, the mark and the submenu indicator SHALL be hidden; each group SHALL be exposed as a group, named by its title where it has one. WHILE trigger is anchor THE menu SHALL also expose on its anchor that it opens a menu and whether the menu is open, where the platform has that property.
MNU-25 WHILE the menu is open THE 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.
MNU-26 WHILE an item's label is empty THE menu SHALL still render the item, and in debug builds report that it has no accessible name.
MNU-27 WHEN the colors of an item change between states THE menu SHALL animate them over comp.menu.motion.state-duration with comp.menu.motion.state-easing, and SHALL change them at once where motion is reduced.
MNU-28 WHILE a group's title is not empty THE menu SHALL draw it above the group's items in comp.menu.group-title.typescale and comp.menu.group-title.color, starting comp.menu.group-title.padding-inline.regular from the rows' start edge and ending comp.menu.group-title.padding-inline.regular from their end edge, whether or not the menu draws the mark and leading icon columns (so that, at the default values, the title starts where each row's first column starts: the mark, the leading icon or the label), with comp.menu.container.padding-block.regular above and below, wrapping and growing with enlarged text rather than clipping; WHEN the layout direction is right-to-left start and end SHALL mirror. The title SHALL take no focus and SHALL NOT be read as an item.
MNU-29 WHEN a radio item is not inside a group, or a group's value names none of its radio items THE menu SHALL draw every such radio item unchecked; WHEN two radio items of one group share a value THE menu SHALL check both while the group's value names it; WHEN a group holds another group THE menu SHALL draw the inner group's items as the outer group's, with no title or divider of the inner group's own; in debug builds it SHALL report each of these.
MNU-30 IF the icon set cannot draw a glyph the menu draws yet — the check or the named glyph of a mark, the check inside a checked box, the submenu indicator — because its font is still loading or failed to load THE menu SHALL draw nothing in the glyph's place, never the glyph's name as text, SHALL keep its column's width, SHALL still draw the box of a checkbox look and the ring of a radio look, SHALL still expose the item's checked state and whether its submenu is open, and SHALL show the glyph once the icon set can draw it, without the fade of MNU-22.
MNU-31 WHILE an item's shortcut is not empty THE menu SHALL draw it in the item's trailing column with the keyboard shortcut component, hosted as KBS-13 says: at size small, in the form comp.menu.shortcut.variant names, with convention auto, and disabled while the item is disabled; its legends and separators SHALL take comp.menu.trailing-text.typescale and the trailing-text color of the item's condition and state, and its caps the keyboard shortcut's own tokens. IF an item has both a shortcut and trailing text THE menu SHALL draw the shortcut, not the trailing text, and in debug builds report the item.
MNU-32 THE 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 menu is shown THE menu SHALL draw itself for the new direction at once, without a motion of its own, keeping its open state and its focus, the surface placed again by the new direction (MNU-2).
MNU-33 WHERE the layout direction is right-to-left THE menu SHALL draw an item's leading icon and trailing icon, the glyph a markIcon names and the submenu indicator 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 MNU-19 gives. THE 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 check — the check of a mark and the check inside a checked box — is never mirrored (MNU-22, MNU-39).
MNU-34 THE menu SHALL lay out an item's label, supporting text and trailing text and a group's title 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 menu SHALL still place each and align its lines by the layout direction, not by the text's own.
MNU-35 THE menu SHALL separate a group from an adjacent group or an adjacent item outside any group as its separation names: WHILE separation is auto, as comp.menu.separation names, which follows sys.behavior.separation unless a design system sets it apart; WHILE separation is divider or gap, as that value names, whatever the tokens say; divider draws the line of MNU-23 and gap leaves the space of MNU-36. Every submenu SHALL separate its groups as its menu does. WHEN separation, or the token it follows, changes THE menu SHALL draw the other form at once, without animation, keeping its open state and its focus; WHEN separation names a value the menu does not define THE menu SHALL treat it as auto and in debug builds report the unknown value. A platform that cannot draw a choice SHALL fall back to the first value, divider, and state that in its README.
MNU-36 WHILE the menu's separation is gap (MNU-35) THE menu SHALL draw each group, and each run of adjacent items outside any group, as a tile of its own: a sheet of comp.menu.surface.color with the corners comp.menu.surface.shape eased by sys.shape.corner-smoothing and the elevation comp.menu.surface.elevation, its ring drawn inside the tile's edge, holding its rows inside comp.menu.surface.padding-block and comp.menu.surface.padding-inline as the surface holds them; it SHALL leave comp.menu.group.gap of empty space between two tiles, through which what is under the menu shows, and SHALL draw no sheet, edge or elevation of its own around the tiles. It SHALL draw no line between tiles and SHALL expose no separator, the groups staying exposed as groups (MNU-24) and a tile being nothing to assistive technology; it SHALL leave no space before the first or after the last tile, and a group's title SHALL keep its own padding inside its tile (MNU-28). The tiles of a menu taller than the room it has SHALL scroll together inside the menu's bounds (MNU-2), and every submenu SHALL draw its tiles the same way: the gap tells the parts of a menu apart by cutting its sheet, as the gap of a button group or of an accordion makes separate shapes, never by empty space on one sheet.
MNU-37 WHILE a checkbox or radio item is checked THE menu SHALL fill its row, within the corners comp.menu.container.shape gives, with the container color of the selected condition in its state (comp.menu.selected.container.color at rest), SHALL draw that fill over its surface (comp.menu.surface.color) and under the row's content, so that a container color that is not opaque lets the surface show through it and nothing else, and SHALL draw its label, supporting text, trailing text and icons in that condition's colors; checked rows next to each other SHALL each keep their own corners and SHALL NOT be joined into one shape. THE menu SHALL NOT show a choice by the fill alone: a checked item always draws its mark (MNU-38), and a design system whose selected container color is none still marks it. The mark is what tells a checked item; the fill is a second cue a design system may give or withhold, through comp.menu.selected.container.color for each contrast scheme, and the menu SHALL read that token in the scheme it is drawn in and SHALL NOT decide the fill by the scheme itself. The default system gives it at medium and high contrast, where a person has asked for stronger cues, and withholds it at standard contrast, so that there the one filled row of a menu is the row the person is on (MNU-58).
MNU-38 THE menu SHALL draw the mark of a checkbox or radio item as the first of these that applies: the glyph the item's markIcon names; the glyph its group's markIcon names; otherwise what the design system chooses for the item's kind — comp.menu.checkbox-item.mark for a checkbox item (checkbox: the checkbox look of MNU-39; check: the icon set's check, as MNU-22 draws it) and comp.menu.radio-item.mark for a radio item (radio: the radio look of MNU-40; check: the same check). Whatever the mark is THE menu SHALL place it in the mark column, the first column of the row (MNU-19), before the leading icon, centered in a box of side comp.menu.mark.size.regular, SHALL hide it from assistive technology, SHALL NOT make it a control of its own (it takes no focus and no press and has no role apart from its item's), and SHALL keep exposing the item as a menu item checkbox or a menu item radio with its checked state (MNU-24). A platform that cannot draw a choice SHALL fall back to the first value and state that in its README.
MNU-39 WHILE a checkbox item's mark is the checkbox look (MNU-38) THE menu SHALL draw, whether the item is checked or not, a box of side comp.menu.control.box.size with corners comp.menu.control.box.shape eased by sys.shape.corner-smoothing: WHILE the item is not checked the box SHALL be empty, with an outline of comp.menu.control.outline.width inside its edge in the control color of its condition and state (comp.menu.unselected.control.color at rest); WHILE it is checked the box SHALL be filled in the control color (comp.menu.selected.control.color at rest), outline and fill being one shape with no seam between them, with the glyph check of the system's icon set centered on it at comp.menu.control.mark.size in the control-mark color of its condition and state, drawn with sys.icon.family.selected, sys.icon.fill.selected, the weight comp.menu.control.mark.weight (not sys.icon.weight: the check in the box is a control's own mark, as the checkbox's is, CHK-12) and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max, not clipped by the box and never mirrored; the weight SHALL change the stroke of the glyph only. THE menu SHALL draw no round state layer behind the box: the row is the item's container.
MNU-40 WHILE a radio item's mark is the radio look (MNU-38) THE menu SHALL draw, whether the item is checked or not, a ring of outer diameter comp.menu.control.ring.size in the control color of its condition and state, the outer diameter the same in both conditions: WHILE the item is not checked the ring SHALL be stroked at comp.menu.control.ring.width inside that diameter; WHILE it is checked it SHALL be stroked at comp.menu.control.ring.selected-width-share of the ring's radius, or at comp.menu.control.ring.width where that is wider, the stroke growing inward, and the area inside the ring's inner edge, less comp.menu.control.fill.inset all round, SHALL be filled in the same control color at comp.menu.control.fill.opacity, ring and fill being one shape where they touch; a fill whose diameter would be zero or less SHALL NOT be drawn. THE menu SHALL draw no dot and no round state layer of its own.
MNU-41 WHEN checked changes while the menu is open and the item's mark is a control look THE menu SHALL move the box's outline and fill, the check inside it, and the ring's stroke and fill from the old condition to the new over comp.menu.motion.state-duration with comp.menu.motion.state-easing — the check fading, the ring's stroke growing or thinning inward — without moving or resizing the box or the ring's outer diameter, and the row's fill SHALL change with it (MNU-27); WHERE motion is reduced it SHALL draw the new condition whole at once.
MNU-42 THE menu SHALL draw a submenu item as an item's row (MNU-19, MNU-20) with the submenu indicator in its trailing icon column: the glyph chevron_right of the system's icon set at comp.menu.trailing-icon.size.regular in the trailing-icon color of the item's condition and state, drawn through the icon helper with sys.icon.family.unselected, sys.icon.fill.unselected, sys.icon.weight and an optical size clamped between sys.icon.optical-size.min and sys.icon.optical-size.max; its trailing text, where it has one, SHALL be drawn in the trailing column before the indicator, and a submenu item SHALL have no shortcut, no trailing icon of the product's and no mark. THE menu SHALL draw no chevron of its own (no path, shape or character of its own).
MNU-43 WHEN the user presses Enter or Space on an enabled submenu item, presses the arrow key that points to the inline end on it (Right Arrow in a left-to-right layout, Left Arrow in a right-to-left one), presses it with a pointer or taps it THE menu SHALL open that item's submenu without delay, close any other open submenu of the same surface, move focus to the submenu's first enabled item (its first item where none is enabled), showing the focus ring only when the input was a key, set the item's open to true and emit the item's toggle once with true; it SHALL NOT emit close and SHALL close nothing else. WHEN the submenu is already open the same input SHALL move focus to its first enabled item and emit nothing. WHEN the product sets a submenu item's open to true while the surface that holds the item is shown THE menu SHALL open the submenu without moving focus and without emitting toggle.
MNU-44 WHEN a pointer that can hover rests on an enabled submenu item for comp.menu.motion.submenu-delay THE menu SHALL open that item's submenu, closing any other open submenu of the same surface, and SHALL leave focus on the submenu item (MNU-8) until the pointer enters the submenu or a key moves it; a pointer that leaves the item sooner SHALL open nothing. Touch has no hover: a tap opens the submenu (MNU-43). WHERE motion is reduced the delay SHALL still apply: it reads the pointer's intent and is not an animation.
MNU-45 WHILE a submenu is open and the pointer moves from its submenu item towards it THE menu SHALL keep the submenu open and SHALL NOT move hover or focus to the items the pointer crosses on the way (MNU-8): the pointer is on its way while it keeps moving inside the triangle whose corners are the pointer's last position on the submenu item and the two corners of the submenu's surface on the side that faces the item. WHEN the pointer leaves that triangle without entering the submenu, or rests inside it for comp.menu.motion.submenu-delay THE menu SHALL treat the item under the pointer as MNU-8 says, which closes the submenu when that item is another one (MNU-48). The triangle SHALL be taken anew each time the pointer leaves the submenu item and SHALL follow the side the submenu is placed on (MNU-46), so that it mirrors in a right-to-left layout and when the submenu is placed on the other side.
MNU-46 THE menu SHALL place a submenu's surface beside the surface that holds its submenu item, comp.menu.submenu.offset from that surface's inline-end edge (to the right in a left-to-right layout, to the left in a right-to-left one), with the block-start edge of its first row level with the block-start edge of the submenu item's row, and SHALL draw it above the surface it opened from; WHERE that side has less room than the surface needs within comp.menu.surface.viewport-margin of the edge of the visible part of the window, above the on-screen keyboard while one is shown, and the inline-start side has more THE menu SHALL place it on the inline-start side instead, the same distance from that edge, and a submenu opened from a surface placed that way SHALL try that side first; it SHALL shift the surface along the block axis to stay within that margin, SHALL limit its height to the least of the height its items need, the most the product's own style gives the submenu's surface (MNU-2) and the room left and scroll its items beyond it, and SHALL keep it in place against its submenu item while that item moves. WHEN the submenu item is scrolled out of the visible part of its surface THE menu SHALL close the submenu (MNU-48).
MNU-47 WHERE neither side of the surface that holds a submenu item has room for the submenu's surface at comp.menu.surface.min-width within comp.menu.surface.viewport-margin THE menu SHALL still present the submenu as a surface of its own, laid over the surface it opened from, on the side with more room and as far towards that side as the margin allows, narrowed down to comp.menu.surface.min-width before it covers more, and SHALL leave at least sys.target.touch of the covered surface's inline size uncovered on its other side wherever the visible part of the window is wide enough for both; THE menu SHALL NOT replace the items of a surface with its submenu's at any window size. WHILE a submenu covers part of the surface it opened from, a press on the uncovered part of that surface SHALL close the submenu as MNU-48 says and SHALL NOT activate the item under the press, so that a person without a keyboard or a back button can go back one level; keys and focus SHALL behave as in a wide window.
MNU-48 WHEN focus is in a submenu and the user presses the arrow key that points to the inline start (Left Arrow in a left-to-right layout, Right Arrow in a right-to-left one), Escape or the platform's back THE menu SHALL close that submenu and every submenu opened from it, and nothing else, return focus to its submenu item, showing the focus ring only when the input was a key, set the item's open to false and emit the item's toggle once with false, without emitting close. WHEN focus or hover moves to another item of the surface that holds the submenu item (MNU-7, MNU-8), WHEN the submenu item becomes disabled or is removed, or WHEN the product sets its open to false THE menu SHALL close the submenu the same way — the product's own change emitting no toggle — and focus that was inside it SHALL move to the submenu item, or to the item that took focus. Escape and the platform's back SHALL close one level for each press, and the menu itself only when no submenu is open (MNU-5). THE menu SHALL keep at most one submenu open in each surface.
MNU-49 WHEN the menu is closed — for a reason MNU-5 or MNU-6 names, at whatever level it arose, or because the product sets open to false — THE menu SHALL close every open submenu with it at once, emit close once and never once for each level, set each closed submenu item's open to false without emitting its toggle, and return focus to the anchor as MNU-1 says (a context menu: to the element MNU-62 names), not to a submenu item; a press inside any surface of the menu SHALL NOT count as a press outside, and focus moving between the menu's surfaces SHALL NOT count as focus leaving it. The surfaces SHALL fade out together as MNU-4 says.
MNU-50 WHILE a submenu is open THE menu SHALL draw its submenu item in the expanded condition (MNU-18) in whatever interaction state it is in, so that the path from the menu to the surface in use stays marked while the pointer and focus are elsewhere, drawing that condition's container color over its surface as MNU-37 draws a checked row's, and SHALL draw the submenu's surface as the menu's own (MNU-3) from the same tokens, its items following every behaviour of the menu's items, with the open and close motion of MNU-4 growing from the edge that faces its submenu item.
MNU-51 WHEN the user presses the arrow key that points to the inline end on an item that is not a submenu item, or the arrow key that points to the inline start in the menu's own surface THE menu SHALL do nothing and SHALL NOT let the key scroll or act on what is behind the menu. WHILE a submenu item's disabled is true THE menu SHALL NOT open its submenu by any input, SHALL close it if it is open (MNU-48), and SHALL still expose that the item opens a menu, with the disabled state (MNU-9).
MNU-52 THE menu SHALL let a submenu hold items of every kind, groups and further submenus included, to any depth, each level behaving as MNU-42 to MNU-51 say, and SHALL keep the radio items of a submenu apart from those of every other surface: a group's value reaches only the radio items that are its own. WHEN a submenu item lies more than two levels of submenu below the menu, or WHEN a submenu holds no item THE menu SHALL still render what it can — a submenu with no item draws its row and its indicator, takes focus and opens nothing — and in debug builds report each. WHILE open is true on a submenu item whose own surface is not shown THE menu SHALL show nothing for it until that surface is shown.
MNU-53 WHILE an item's href is not empty THE menu SHALL make the item a link item: the platform's own link to href (the web's anchor element; SwiftUI's Link; on Compose, Flutter and React Native an element with the platform's link semantics whose address is opened by the platform's opener), as BTN-22 says for a link button, so that whatever the platform gives a link — on the web its context menu, opening in a new tab or window, copying and dragging its address, and the address shown before it is followed — is the platform's, and not the menu's to draw or to take over. WHERE the platform has a menu item role (the web) THE menu SHALL expose a link item with that role, on the link itself, and not with the link role: a menu's own role, an item's position in it and the keys of MNU-7 hold only while everything the menu holds is a menu item, and a menu of links is operated as a menu; WHERE the platform's menu items are buttons and it has no such role THE link item SHALL carry the platform's link role or trait in place of the button's. Everything this spec says of an item (an action) other than what MNU-54 to MNU-57 say — its row and columns, its conditions, destructive included, its states, focus ring, minimum height, press answer, corners and motion, its place in the order of the arrow keys, Home, End and typed characters, and focus following the pointer (MNU-7, MNU-8, MNU-10 to MNU-21, MNU-27, MNU-31) — SHALL hold for a link item from the same tokens, with no look of its own for a link and none for a destination already visited; a link item SHALL NOT be a Tab stop, as no item of a menu is.
MNU-54 WHEN the user activates an enabled link item at any level as an item of a menu is activated — by a click with the primary button and no modifier key, a touch, Enter or Space with no modifier key, or the default action of assistive technology — THE menu SHALL emit the item's click exactly once, then emit close with the reason item and be closed as MNU-6 says for an action, and then open href where target names, unless the product cancelled that click, in which case it SHALL still close and SHALL open nothing: the product's own router has taken the navigation. It SHALL show the pressed state as MNU-15 and MNU-16 say and SHALL NOT hold the closing or the navigation back for sys.motion.duration.press-hold. Space SHALL activate a link item exactly as Enter does and SHALL scroll nothing: a link item is exposed and operated as a menu item (MNU-53), nothing in its look tells it from the action in the next row, Space activates every other row of the menu, and behind an open menu Space has no page to scroll (MNU-51). For every other activation the platform gives a link — on the web a click or Enter with a modifier key, a middle click and the link's context menu, on Apple platforms a long press that previews — THE menu SHALL leave the link to the platform, SHALL prevent, replace or imitate none of it, SHALL NOT emit close and SHALL stay open at every level with focus where it was, so that a person can open several of a menu's links in other tabs one after another; what MNU-5 says of a press outside and of focus leaving the menu still holds.
MNU-55 WHILE an item is a link item and its target is new THE menu SHALL open href in a new context — on the web a new tab or window that is given no handle to the page that opened it; on the other platforms outside the screen the person is on, as the platform's opener decides — and, WHILE the item's trailingIcon is empty, SHALL draw the glyph open_in_new of the system's icon set in the item's trailing icon column, as MNU-21 draws trailingIcon: at comp.menu.trailing-icon.size.regular, in the trailing-icon color of the item's condition and state (comp.menu.unselected.trailing-icon.color at rest, comp.menu.destructive.trailing-icon.color for a destructive item), through the icon helper, which mirrors it in a right-to-left layout because the icon set marks its name (MNU-33), hidden from assistive technology, and held to MNU-30 while the icon set cannot draw it. The mark SHALL reserve the trailing icon column in its surface as a trailing icon does (MNU-19), and the item's shortcut or trailing text SHALL keep its own column before it: the mark stands where a glyph of the product's would, not where the keys are written. WHILE trailingIcon names a glyph THE menu SHALL draw that glyph and no mark of its own: the product has chosen what that place shows. Whether a mark is drawn or not THE menu SHALL expose targetLabel as the end of the item's description, after its supporting text and whatever else MNU-24 puts there, and WHILE targetLabel is empty SHALL still render and in debug builds report that a link that opens a new context does not say so. WHILE target is same THE menu SHALL draw no mark and SHALL ignore targetLabel.
MNU-56 WHILE a link item's disabled is true THE menu SHALL NOT make it a link that can be followed: it SHALL render it as MNU-13 says, the mark of MNU-55 with it, SHALL expose it as MNU-53 exposes a link item, with the disabled state and no destination, SHALL keep it reachable by the arrow keys as MNU-7 keeps any disabled item, SHALL NOT emit its click or close for it, and SHALL open nothing by any means the platform gives a link (on the web the anchor carries no href, so there is no context menu entry, no drag and no new tab for it).
MNU-57 THE menu SHALL give a destination to the item kind alone. A checkbox item and a radio item hold a state and a submenu item opens another level, so none of them has href, target or targetLabel: a control that holds a state, or a second action, is not a destination; WHERE a platform lets one be written on them (an attribute of the web's element, a field of React Native's item object) THE menu SHALL ignore it, keep the item what it is, and in debug builds report it. WHEN a link item's target names a value the menu does not define THE menu SHALL treat it as same, and in debug builds report the unknown value. IF an item's href is only white space, or names a script to run rather than a place to go (the web's javascript: scheme), or cannot be read as an address by the platform, THE menu SHALL make the item an action without a destination, and in the last two cases report it in debug builds, as BTN-26 says for a button. WHILE an item's href is empty THE menu SHALL ignore its target and targetLabel.
MNU-58 WHILE a link item's current is true THE menu SHALL expose it as the current item of its set (on the web aria-current with the value page, on the item's link; on a platform without that state, the selected state) and SHALL draw its row from the current condition: a fill as MNU-37 lays one, in comp.menu.current.container.color at rest, and that condition's colors for its label, supporting text, trailing text and icons, its icons in the selected family and fill, in every state and in every contrast scheme; it SHALL draw no mark in the mark column, keep its label's own weight, and SHALL keep it a link item in every other respect (MNU-53 to MNU-56). The current destination is where the person is, not a choice the menu holds: it carries no check, and it is told from its neighbours by the fill the menu draws for the row the person is on, as a navigation drawer marks its current destination, while assistive technology is told by the state. Its fill is its own and not a checked row's: a checked row has its mark and may go without a fill (MNU-37), the current row has only its fill, so a design system that withholds the one keeps the other. WHERE the platform draws with a person's own forced colors, which remove the fill, THE menu SHALL draw a line of sys.border.thick inside the current item's row in the platform's link color. THE menu SHALL NOT set current itself on any activation: where the product is, is the product's to say. WHILE an item's href is empty THE menu SHALL ignore its current and in debug builds report it.
MNU-59 THE menu SHALL decide the mark column and the leading icon column for each section of a surface — a group, with the items of a group inside it (MNU-29), or a run of adjacent items outside any group: the parts MNU-36 draws as tiles — and each of the two columns by itself. WHILE iconSpace is auto a row SHALL keep a column, empty where it draws nothing in it, WHERE an item of its section draws in it — a checkbox or radio item its mark, whether checked or not and whatever the mark is (MNU-38), an item its leading icon — so that the labels of a section start on one line, and a section where no item draws in a column SHALL keep no room for it, its labels starting that much nearer the row's start; WHILE iconSpace is always every row SHALL keep both columns, whether or not any item of the surface draws in them; WHILE iconSpace is never a row SHALL have a column only where it draws in it itself. The room kept is the column's box (comp.menu.mark.size.regular for the mark, comp.menu.leading-icon.size.regular for the icon) and comp.menu.container.gap.regular after it, draws nothing and is nothing to assistive technology. A submenu item has no mark and so draws nothing in the mark column (MNU-42). Each surface SHALL be decided by itself, every submenu with the menu's iconSpace; a group's title SHALL keep its own place whatever the columns of its section are (MNU-28). WHEN an item is added or removed, or an item's kind or leadingIcon changes, THE menu SHALL decide the item's section again; WHEN iconSpace names a value the menu does not define THE menu SHALL treat it as auto and in debug builds report the unknown value.
MNU-60 WHILE trigger is context, WHEN the person asks for a context menu in the anchor's area — a secondary click (a right click, a Control-click, a two-finger click on a trackpad), the context-menu command of assistive technology, a press of a touch or a pen held within the platform's touch slop (8 points where the platform names none) for the platform's long-press duration (500 ms where the platform names none), or Shift+F10 or the ContextMenu key with no other modifier while focus is on the area or on an element inside it — THE menu SHALL emit contextRequest once, with the point in the viewport's coordinates (where the pointer is, where the press began, or the block-end, inline-start corner of the element that has focus), the element under the pointer or with focus, and how it was asked (pointer, long-press or keyboard), and SHALL NOT open itself: the product sets open to true (MNU-1). Unless the product cancels contextRequest, THE menu SHALL keep the platform's own context menu from answering the request, and after a long press SHALL keep the release of that press from activating what is under it; a cancelled request SHALL be left to the platform. A request that an element inside the area has already taken (on the web, a contextmenu event whose default is prevented) and one made inside the menu's own surfaces SHALL NOT be answered. WHILE trigger is anchor THE menu SHALL answer none of these. WHEN trigger names a value the menu does not define THE menu SHALL treat it as anchor and in debug builds report the unknown value.
MNU-61 WHILE trigger is context THE menu SHALL place its surface at the point of the last contextRequest, with no offset, as placement names it from the point (bottom-start: the surface's block-start, inline-start corner on the point; bottom-end: its block-start, inline-end corner; top-start and top-end: its block-end corners); WHERE the side below or above the point has less room than the surface needs within comp.menu.surface.viewport-margin of the edge of the visible part of the window, above the on-screen keyboard while one is shown, and the other side has more THE menu SHALL flip to the other side of the point, and WHERE the side before or after the point along the line has less room than the surface's width and the other has more SHALL flip along the line as well, each axis by itself, so that a surface asked for near a corner of the window opens away from that corner with a corner of its own on the point; it SHALL then shift to stay within that margin, limit its height as MNU-2 says and scroll its items beyond it, and keep the point where it is in the area while the area moves. FOR a request by the keyboard THE menu SHALL place the surface against the element that had focus as MNU-2 places it against an anchor, offset included. WHEN open becomes true with no request since the menu was last closed THE menu SHALL place the surface against the area as MNU-2 says. The surface SHALL grow from its edge that faces the point or the element (MNU-4).
MNU-62 WHILE trigger is context THE menu SHALL behave as every other menu does once it is open — the same items, focus on opening (MNU-1), keys, typeahead, submenus, activation and close reasons — and WHEN it closes SHALL return focus, where focus was inside it, to the element that had focus when the request was made (the element a keyboard request was made on), and leave focus where the platform puts it where no element had it; Tab and Shift+Tab SHALL move focus on from that element. A press anywhere outside the menu's surfaces, the area included, SHALL count as a press outside (MNU-5); a secondary click or a long press elsewhere while the menu is open SHALL therefore close it and, in the area, request it again at the new point.
MNU-63 WHILE trigger is context THE menu SHALL NOT expose on its area that it opens a menu, whether it is open or which element it is (no aria-haspopup, aria-expanded or aria-controls on the web), because the area is not a button: a context menu is found by the platform's own convention — a secondary click, a long press, Shift+F10, the ContextMenu key, the context-menu command of assistive technology. THE menu SHALL NOT be named by its area; it SHALL take the name the product gives it (aria-label on the web), and have none where the product gives none, as the platform's own context menus have none.
MNU-64 THE menu SHALL draw the rows of every surface at its density. WHILE density is regular each row SHALL take the regular value of every token of the density axis this spec names (comp.menu.container.height.regular, comp.menu.container.padding-block.regular, comp.menu.container.padding-inline.regular, comp.menu.container.gap.regular, comp.menu.leading-icon.size.regular, comp.menu.trailing-icon.size.regular, comp.menu.mark.size.regular, comp.menu.group-title.padding-inline.regular); WHILE density is compact it SHALL take the compact value of each in its place (comp.menu.container.height.compact, comp.menu.container.padding-block.compact, comp.menu.container.padding-inline.compact, comp.menu.container.gap.compact, comp.menu.leading-icon.size.compact, comp.menu.trailing-icon.size.compact, comp.menu.mark.size.compact, comp.menu.group-title.padding-inline.compact), so that a compact row is lower, set in less from its edges and draws smaller glyphs, the room a row keeps for an empty column (MNU-59) shrinking with them. The text styles, the surface and its paddings, the dividers, the group gap and the control of a checkbox or radio item SHALL be the same in both. Every submenu SHALL take the menu's density. WHEN density names a value the active design system does not have THE menu SHALL draw the default density's rows and in debug builds report the unknown value; a density a design system added SHALL be drawn from that density's tokens, as the core values are. WHEN density changes while the menu is open THE menu SHALL draw the new rows at once, without a motion, keeping focus where it is.
MNU-65 WHEN the element the open menu is placed against — the anchor; for a context menu its area, or the element a keyboard request was made on (MNU-61) — is no longer drawn: taken out of the page, given no box (on the web display none on it or on an ancestor), made invisible (visibility hidden) or skipped by its platform's content visibility (on the web content-visibility hidden, or auto while it is off screen), THE menu SHALL emit close once with the reason anchor-hidden and be closed as MNU-5 says, every open submenu with it (MNU-49), and SHALL NOT place its surface against that element while it is not drawn, because a surface with nothing beside it no longer says what it belongs to. WHEN it closes with focus inside it THE menu SHALL return focus as MNU-49 says and, where that element cannot take focus, SHALL NOT keep focus inside its closed surfaces. WHEN the element is drawn again while the menu is still open THE menu SHALL follow it again, and emit close again the next time it is no longer drawn.
MNU-66 WHERE the platform has ARIA attributes (the web), WHILE an item is a link item its role is carried by its anchor (MNU-53), and WHEN the product sets an aria- attribute on its host, by the attribute or by the ARIA property that reflects it, THE 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 that anchor — an id reference by the elements it names in the host's tree or a tree around it, a description after its own — except those the item owns (aria-disabled, aria-current, aria-keyshortcuts), which it SHALL move, not expose and report in debug builds; its label is its name over a name given so, and while label is empty a name given so is its name. WHEN the item stops being a link item THE menu SHALL give back to its host what it moved, as it was written, and WHEN it becomes one SHALL move what the host carries. WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it (ariaLabel, ariaDescribedByElements…) to null, THE menu SHALL take it off the anchor.
MNU-67 WHERE the platform has ARIA attributes (the web), the hosts of the menu, of an item that is not a link item (a checkbox item, a radio item and a submenu item included) and of a group carry their roles themselves (MNU-24), so what the product writes on them is not moved, but for a name: WHILE an item's or a group's label, or the anchor that names a menu, is not empty THE menu SHALL hold the product's aria-label and aria-labelledby on that host as their data-aria- twins, so that its own is the whole name although the platform gives aria-labelledby the precedence, and WHEN it is emptied SHALL give them back as they were written; WHILE it is empty a name the product gives so is the name and an item SHALL NOT be reported as having none in debug builds (MNU-26) — a context menu is named so (MNU-63). WHILE a menu is closed, or a group is in a group (MNU-29), it exposes nothing, and THE menu SHALL hold them as well, giving them back when it is exposed again. WHEN the product removes one, or its data-aria- twin, or sets the ARIA property that reflects it to null, THE menu SHALL take it away.
MNU-68 WHEN a surface of the menu is shown — the menu's own as it opens, a submenu's as its item opens it — THE menu SHALL show that surface's list from its start: a list taller than the surface (MNU-2, MNU-46) SHALL open scrolled to its top, so that its first row, its first group's title included, is in view, and WHERE the item that takes focus (MNU-1, MNU-43, or the last enabled item a menubar opens its menu on, MBR-9) is not whole in view there, scrolled just far enough to bring that item whole into view and no further. WHEN focus moves to an item by a key or by the menu's own doing (MNU-7, MNU-48) THE menu SHALL scroll the list that holds it just far enough to bring the item whole into view, and not at all where it is whole in view already — the first item of the list together with the list's start, the last together with its end, and the first item of a titled group together with that title (MNU-28); focus that follows the pointer (MNU-8) SHALL scroll nothing. Whether the item is in view SHALL be judged as the surface stands at rest: while its opening motion runs (MNU-4) the list SHALL keep its scroll position until the surface stands at rest, and then bring the item that has focus into view as this sentence says, so that no frame of the motion moves the list.
MNU-69 THE menu SHALL break the lines of an item's label, supporting text and trailing text and of a group's title by the rules of the text's language: a line of Japanese or Chinese SHALL neither begin with closing punctuation, a small kana, the prolonged sound mark (ー) or an iteration mark nor end with opening punctuation (the strict rules of line breaking that JIS X 4051 and the W3C's requirements for Japanese text layout set out), whether or not the product marks the text's language; and WHERE the platform finds the phrases of Japanese text (on the web, word-break: auto-phrase, which reads the language the product marks), a line of Japanese SHALL break between two phrases rather than inside one. A phrase or a word longer than its line SHALL still break rather than overflow (MNU-20).

各平台名称对照

对等并不意味着 API 完全相同。同一份契约,由各平台以自己的惯用方式表达。

规格 Web SwiftUI Jetpack Compose Flutter React Native
组件 tanrei-menu TanreiMenu TanreiMenu TanreiMenuAnchor Menu
open · 属性 open isPresented expanded controller visible
placement · 属性 placement placement placement placement placement
trigger · 属性 trigger trigger trigger trigger trigger
separation · 属性 separation separation separation separation separation
iconSpace · 属性 iconSpace iconSpace iconSpace iconSpace iconSpace
density · 属性 density density density density density
close · 事件 close isPresented onDismissRequest onClose onDismiss
contextRequest · 事件 contextrequest onContextRequest onContextRequest onContextRequest onContextRequest
备注 tanrei-menu (open, placement, separation, and anchor: the id of the anchor element, as the HTML anchor attribute) holds tanrei-menu-item, tanrei-menu-checkbox-item, tanrei-menu-radio-item, tanrei-submenu and tanrei-menu-group children; item attributes are kebab-case (supporting-text, leading-icon, trailing-icon, trailing-text, mark-icon). click, change and toggle fire on the item or group and bubble, so one listener on the menu can take them all. The surface is a manual popover in the top layer, placed with CSS anchor positioning and a script fallback where the browser has none; each submenu's surface is a popover of its own, placed against its tanrei-submenu the same way. The menu implements its own light dismiss so that it can emit close with a reason, and treats every surface of its tree as inside. A press outside also reaches the element under it. It sets aria-haspopup="menu", aria-controls and aria-expanded on the anchor element, and the same three on each submenu item's row. The inline attribute, for tests, draws the open surface in place, with each open submenu beside it (MNU-19). The mark is the mark part: the box or the ring of a control look, drawn by the menu's own styles from --tanrei-comp-menu-control-*, or the icon helper's glyph (MNU-22, MNU-38). The submenu indicator is the icon helper's chevron_right in the submenu-indicator part. A group's title is inset by --tanrei-comp-menu-group-title-padding-inline-regular from the rows' edges (MNU-28). The link item is not built on this platform yet: MNU-53 to MNU-57 and what MNU-6, MNU-19 and MNU-24 say of it, with the fixtures links, links-rtl and links-trailing. A link item's current writes aria-current="page" on its anchor and draws the row from the current condition (MNU-58). Under gap each group, and each run of items outside a group, is a .tile (part tile) of its own inside the scroll box; under divider the tiles are boxes with no look (MNU-36). trigger is the trigger attribute; with context, anchor names the area. contextRequest is contextrequest, a cancelable event whose detail is { x, y, element, input }: client coordinates, the element under the pointer or with focus, and pointer, long-press or keyboard. The menu listens on the area for contextmenu (a secondary click, Android's long press, the context-menu command of assistive technology) and cancels it unless the product cancels the request; for a touch or a pen held still, which WebKit on iOS answers with no contextmenu; and for Shift+F10 and the ContextMenu key on keydown, whose own contextmenu it then cancels. After a long press the release of that press, and its click, go no further. On iOS a long press may also select text: an area whose text is not to be selected says so in its own styles (user-select, -webkit-touch-callout). A modifier on the anchor, .tanreiMenu(isPresented:placement:) { … }, as .popover, presented with presentationCompactAdaptation(.popover) so it stays a popover on iPhone; a press outside does not reach what is under it. The content builder takes the item views. disabled follows .disabled(_:). Closing writes false to isPresented, which is close. SwiftUI's own Menu is not used: its items cannot be tokenised. Ships the previous revision today (the check stroked as a path in a 16 box, a group's title starting where the labels start). Not built on this platform yet, among the rest of this spec: the submenu item kind and everything about it (MNU-42 to MNU-52), separation (MNU-35, MNU-36), the checkbox and radio looks of the mark and markIcon (MNU-38 to MNU-41) and their 17 fixtures; the selected fill and colors arrive through the tokens, the fill of a checked row with them: none at standard contrast in the default system, as MNU-37 says. Reading comp.menu.separation, comp.menu.checkbox-item.mark and comp.menu.radio-item.mark as typed choices needs this platform's token output to write a component's choice as a typed enumeration, which it does not yet. Nor is the link item: MNU-53 to MNU-57 and what MNU-6, MNU-19 and MNU-24 say of it, with the fixtures links, links-rtl and links-trailing. Lagging behind MNU-58 (a link item's current, drawn with the selected fill) and MNU-36 (the groups as separate tiles under gap): it has neither yet. Not built yet: iconSpace (MNU-59) — this platform still keeps the mark and leading icon columns in every row of a surface where any item draws in them. Not built yet: the context trigger (MNU-60 to MNU-63) — this platform opens the menu from an anchor only. Compose idiom, as DropdownMenu: TanreiMenu(expanded, onDismissRequest, placement) { TanreiMenuItem(text, onClick, …); TanreiMenuGroup { … } } inside the anchor's Box, shown in a Popup; the item composables are called in its content lambda. The caller closes it, so close is onDismissRequest and it carries the reason; the caller also keeps each checkbox item's checked (state hoisting). A press outside does not reach what is under it. enabled is the inverse flag. Ships the previous revision today (the check stroked as a path in a 16 box, a group's title starting where the labels start). Not built on this platform yet, among the rest of this spec: the submenu item kind and everything about it (MNU-42 to MNU-52), separation (MNU-35, MNU-36), the checkbox and radio looks of the mark and markIcon (MNU-38 to MNU-41) and their 17 fixtures; the selected fill and colors arrive through the tokens, the fill of a checked row with them: none at standard contrast in the default system, as MNU-37 says. Reading comp.menu.separation, comp.menu.checkbox-item.mark and comp.menu.radio-item.mark as typed choices needs this platform's token output to write a component's choice as a typed enumeration, which it does not yet. Nor is the link item: MNU-53 to MNU-57 and what MNU-6, MNU-19 and MNU-24 say of it, with the fixtures links, links-rtl and links-trailing. Lagging behind MNU-58 (a link item's current, drawn with the selected fill) and MNU-36 (the groups as separate tiles under gap): it has neither yet. Not built yet: iconSpace (MNU-59) — this platform still keeps the mark and leading icon columns in every row of a surface where any item draws in them. Not built yet: the context trigger (MNU-60 to MNU-63) — this platform opens the menu from an anchor only. Flutter idiom, as MenuAnchor: TanreiMenuAnchor(controller:, placement:, menuChildren: […], builder:) shown in the Overlay; the item widgets go in menuChildren. MenuController.open() and .close() are open, onClose is close. A press outside also reaches what is under it (consumeOutsideTap: false). Ships the previous revision today (the check stroked as a path in a 16 box, a group's title starting where the labels start). Not built on this platform yet, among the rest of this spec: the submenu item kind and everything about it (MNU-42 to MNU-52), separation (MNU-35, MNU-36), the checkbox and radio looks of the mark and markIcon (MNU-38 to MNU-41) and their 17 fixtures; the selected fill and colors arrive through the tokens, the fill of a checked row with them: none at standard contrast in the default system, as MNU-37 says. Reading comp.menu.separation, comp.menu.checkbox-item.mark and comp.menu.radio-item.mark as typed choices needs this platform's token output to write a component's choice as a typed enumeration, which it does not yet. Nor is the link item: MNU-53 to MNU-57 and what MNU-6, MNU-19 and MNU-24 say of it, with the fixtures links, links-rtl and links-trailing. Lagging behind MNU-58 (a link item's current, drawn with the selected fill) and MNU-36 (the groups as separate tiles under gap): it has neither yet. Not built yet: iconSpace (MNU-59) — this platform still keeps the mark and leading icon columns in every row of a surface where any item draws in them. Not built yet: the context trigger (MNU-60 to MNU-63) — this platform opens the menu from an anchor only. <Menu visible anchorRef placement onDismiss items={[…]}>: items is an array whose element type is the union of the five kinds, told apart by a kind field ('item', 'checkbox-item', 'radio-item', 'submenu', 'group'). Presented on the overlay layer that TanreiProvider renders (not a Modal, which would make the rest of the screen unreachable) and placed by measuring the anchor (measureInWindow); Android's back button (BackHandler) is close. A press outside closes the menu and does not reach what is under it. Controlled: the caller sets visible to false from onDismiss and keeps each checkbox item's checked and each group's value. Ships the previous revision today (the check stroked as a path in a 16 box, a group's title starting where the labels start). Not built on this platform yet, among the rest of this spec: the submenu item kind and everything about it (MNU-42 to MNU-52), separation (MNU-35, MNU-36), the checkbox and radio looks of the mark and markIcon (MNU-38 to MNU-41) and their 17 fixtures; the selected fill and colors arrive through the tokens, the fill of a checked row with them: none at standard contrast in the default system, as MNU-37 says. Reading comp.menu.separation, comp.menu.checkbox-item.mark and comp.menu.radio-item.mark as typed choices needs this platform's token output to write a component's choice as a typed enumeration, which it does not yet. Nor is the link item: MNU-53 to MNU-57 and what MNU-6, MNU-19 and MNU-24 say of it, with the fixtures links, links-rtl and links-trailing. Lagging behind MNU-58 (a link item's current, drawn with the selected fill) and MNU-36 (the groups as separate tiles under gap): it has neither yet. Not built yet: iconSpace (MNU-59) — this platform still keeps the mark and leading icon columns in every row of a surface where any item draws in them. Not built yet: the context trigger (MNU-60 to MNU-63) — this platform opens the menu from an anchor only.

令牌

令牌 引用 解析值(浅色)
--tanrei-comp-menu-checkbox-item-mark — 0
--tanrei-comp-menu-container-gap-compact comp.tree-view.container.gap.compact 8px
--tanrei-comp-menu-container-gap-regular sys.space.space150 12px
--tanrei-comp-menu-container-height-compact comp.tree-view.container.height.compact 32px
--tanrei-comp-menu-container-height-regular — 48px
--tanrei-comp-menu-container-padding-block-compact comp.tree-view.container.padding-block.compact 6px
--tanrei-comp-menu-container-padding-block-regular — 8px
--tanrei-comp-menu-container-padding-inline-compact comp.tree-view.container.padding-inline.compact 8px
--tanrei-comp-menu-container-padding-inline-regular sys.space.space150 12px
--tanrei-comp-menu-container-shape — 4px
--tanrei-comp-menu-control-box-shape — 2px
--tanrei-comp-menu-control-box-size — 18px
--tanrei-comp-menu-control-fill-inset — 0px
--tanrei-comp-menu-control-fill-opacity sys.state.selected-state-layer-opacity 0.16
--tanrei-comp-menu-control-mark-size sys.icon.small 20px
--tanrei-comp-menu-control-mark-weight sys.icon.mark.weight 500
--tanrei-comp-menu-control-outline-width sys.border.thick 2px
--tanrei-comp-menu-control-ring-selected-width-share — 0.5
--tanrei-comp-menu-control-ring-size — 20px
--tanrei-comp-menu-control-ring-width sys.border.thick 2px
--tanrei-comp-menu-current-container-color sys.color.secondary-container #262626 · 16%
--tanrei-comp-menu-current-control-mark-color — #000000 · 0%
--tanrei-comp-menu-current-control-color — #000000 · 0%
--tanrei-comp-menu-current-disabled-container-color — #1b1b1b · 12%
--tanrei-comp-menu-current-disabled-control-mark-color — #1b1b1b · 38%
--tanrei-comp-menu-current-disabled-control-color — #1b1b1b · 38%
--tanrei-comp-menu-current-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-menu-current-disabled-leading-icon-color — #1b1b1b · 38%
--tanrei-comp-menu-current-disabled-mark-color — #1b1b1b · 38%
--tanrei-comp-menu-current-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-menu-current-disabled-supporting-text-color — #1b1b1b · 38%
--tanrei-comp-menu-current-disabled-trailing-icon-color — #1b1b1b · 38%
--tanrei-comp-menu-current-disabled-trailing-text-color — #1b1b1b · 38%
--tanrei-comp-menu-current-focused-container-color — #262626 · 24.4%
--tanrei-comp-menu-current-focused-control-mark-color — #000000 · 0%
--tanrei-comp-menu-current-focused-control-color — #000000 · 0%
--tanrei-comp-menu-current-focused-label-color — #262626
--tanrei-comp-menu-current-focused-leading-icon-color — #262626
--tanrei-comp-menu-current-focused-mark-color — #000000 · 0%
--tanrei-comp-menu-current-focused-state-layer-color — #262626
--tanrei-comp-menu-current-focused-supporting-text-color — #262626
--tanrei-comp-menu-current-focused-trailing-icon-color — #262626
--tanrei-comp-menu-current-focused-trailing-text-color — #262626
--tanrei-comp-menu-current-hovered-container-color — #262626 · 22.7%
--tanrei-comp-menu-current-hovered-control-mark-color — #000000 · 0%
--tanrei-comp-menu-current-hovered-control-color — #000000 · 0%
--tanrei-comp-menu-current-hovered-label-color — #262626
--tanrei-comp-menu-current-hovered-leading-icon-color — #262626
--tanrei-comp-menu-current-hovered-mark-color — #000000 · 0%
--tanrei-comp-menu-current-hovered-state-layer-color — #262626
--tanrei-comp-menu-current-hovered-supporting-text-color — #262626
--tanrei-comp-menu-current-hovered-trailing-icon-color — #262626
--tanrei-comp-menu-current-hovered-trailing-text-color — #262626
--tanrei-comp-menu-current-label-color sys.color.on-secondary-container #262626
--tanrei-comp-menu-current-leading-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-menu-current-mark-color — #000000 · 0%
--tanrei-comp-menu-current-pressed-container-color — #262626 · 24.4%
--tanrei-comp-menu-current-pressed-control-mark-color — #000000 · 0%
--tanrei-comp-menu-current-pressed-control-color — #000000 · 0%
--tanrei-comp-menu-current-pressed-label-color — #262626
--tanrei-comp-menu-current-pressed-leading-icon-color — #262626
--tanrei-comp-menu-current-pressed-mark-color — #000000 · 0%
--tanrei-comp-menu-current-pressed-state-layer-color — #262626
--tanrei-comp-menu-current-pressed-supporting-text-color — #262626
--tanrei-comp-menu-current-pressed-trailing-icon-color — #262626
--tanrei-comp-menu-current-pressed-trailing-text-color — #262626
--tanrei-comp-menu-current-state-layer-color comp.menu.current.label.color #262626
--tanrei-comp-menu-current-supporting-text-color sys.color.on-secondary-container #262626
--tanrei-comp-menu-current-trailing-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-menu-current-trailing-text-color sys.color.on-secondary-container #262626
--tanrei-comp-menu-destructive-container-color — #000000 · 0%
--tanrei-comp-menu-destructive-control-mark-color — #000000 · 0%
--tanrei-comp-menu-destructive-control-color — #000000 · 0%
--tanrei-comp-menu-destructive-disabled-container-color — #000000 · 0%
--tanrei-comp-menu-destructive-disabled-control-mark-color — #1b1b1b · 38%
--tanrei-comp-menu-destructive-disabled-control-color — #1b1b1b · 38%
--tanrei-comp-menu-destructive-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-menu-destructive-disabled-leading-icon-color — #1b1b1b · 38%
--tanrei-comp-menu-destructive-disabled-mark-color — #000000 · 0%
--tanrei-comp-menu-destructive-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-menu-destructive-disabled-supporting-text-color — #1b1b1b · 38%
--tanrei-comp-menu-destructive-disabled-trailing-icon-color — #1b1b1b · 38%
--tanrei-comp-menu-destructive-disabled-trailing-text-color — #1b1b1b · 38%
--tanrei-comp-menu-destructive-focused-container-color — #cd262f · 10%
--tanrei-comp-menu-destructive-focused-control-mark-color — #000000 · 0%
--tanrei-comp-menu-destructive-focused-control-color — #000000 · 0%
--tanrei-comp-menu-destructive-focused-label-color — #cd262f
--tanrei-comp-menu-destructive-focused-leading-icon-color — #cd262f
--tanrei-comp-menu-destructive-focused-mark-color — #000000 · 0%
--tanrei-comp-menu-destructive-focused-state-layer-color — #cd262f
--tanrei-comp-menu-destructive-focused-supporting-text-color — #4b4b4b
--tanrei-comp-menu-destructive-focused-trailing-icon-color — #4b4b4b
--tanrei-comp-menu-destructive-focused-trailing-text-color — #4b4b4b
--tanrei-comp-menu-destructive-hovered-container-color — #cd262f · 8%
--tanrei-comp-menu-destructive-hovered-control-mark-color — #000000 · 0%
--tanrei-comp-menu-destructive-hovered-control-color — #000000 · 0%
--tanrei-comp-menu-destructive-hovered-label-color — #cd262f
--tanrei-comp-menu-destructive-hovered-leading-icon-color — #cd262f
--tanrei-comp-menu-destructive-hovered-mark-color — #000000 · 0%
--tanrei-comp-menu-destructive-hovered-state-layer-color — #cd262f
--tanrei-comp-menu-destructive-hovered-supporting-text-color — #4b4b4b
--tanrei-comp-menu-destructive-hovered-trailing-icon-color — #4b4b4b
--tanrei-comp-menu-destructive-hovered-trailing-text-color — #4b4b4b
--tanrei-comp-menu-destructive-label-color sys.color.error #cd262f
--tanrei-comp-menu-destructive-leading-icon-color sys.color.error #cd262f
--tanrei-comp-menu-destructive-mark-color — #000000 · 0%
--tanrei-comp-menu-destructive-pressed-container-color — #cd262f · 10%
--tanrei-comp-menu-destructive-pressed-control-mark-color — #000000 · 0%
--tanrei-comp-menu-destructive-pressed-control-color — #000000 · 0%
--tanrei-comp-menu-destructive-pressed-label-color — #cd262f
--tanrei-comp-menu-destructive-pressed-leading-icon-color — #cd262f
--tanrei-comp-menu-destructive-pressed-mark-color — #000000 · 0%
--tanrei-comp-menu-destructive-pressed-state-layer-color — #cd262f
--tanrei-comp-menu-destructive-pressed-supporting-text-color — #4b4b4b
--tanrei-comp-menu-destructive-pressed-trailing-icon-color — #4b4b4b
--tanrei-comp-menu-destructive-pressed-trailing-text-color — #4b4b4b
--tanrei-comp-menu-destructive-state-layer-color comp.menu.destructive.label.color #cd262f
--tanrei-comp-menu-destructive-supporting-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-destructive-trailing-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-destructive-trailing-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-disabled-container-shape — 4px
--tanrei-comp-menu-disabled-opacity — 1
--tanrei-comp-menu-divider-color sys.color.outline-variant #000000 · 8%
--tanrei-comp-menu-divider-space sys.space.space50 4px
--tanrei-comp-menu-divider-thickness sys.border.hairline 1px
--tanrei-comp-menu-expanded-container-color sys.color.surface-container-high #1b1b1b · 8%
--tanrei-comp-menu-expanded-control-mark-color — #000000 · 0%
--tanrei-comp-menu-expanded-control-color — #000000 · 0%
--tanrei-comp-menu-expanded-disabled-container-color — #1b1b1b · 8%
--tanrei-comp-menu-expanded-disabled-control-mark-color — #1b1b1b · 38%
--tanrei-comp-menu-expanded-disabled-control-color — #1b1b1b · 38%
--tanrei-comp-menu-expanded-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-menu-expanded-disabled-leading-icon-color — #1b1b1b · 38%
--tanrei-comp-menu-expanded-disabled-mark-color — #1b1b1b · 38%
--tanrei-comp-menu-expanded-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-menu-expanded-disabled-supporting-text-color — #1b1b1b · 38%
--tanrei-comp-menu-expanded-disabled-trailing-icon-color — #1b1b1b · 38%
--tanrei-comp-menu-expanded-disabled-trailing-text-color — #1b1b1b · 38%
--tanrei-comp-menu-expanded-focused-container-color — #1b1b1b · 10%
--tanrei-comp-menu-expanded-focused-control-mark-color — #000000 · 0%
--tanrei-comp-menu-expanded-focused-control-color — #000000 · 0%
--tanrei-comp-menu-expanded-focused-label-color — #1b1b1b
--tanrei-comp-menu-expanded-focused-leading-icon-color — #4b4b4b
--tanrei-comp-menu-expanded-focused-mark-color — #000000 · 0%
--tanrei-comp-menu-expanded-focused-state-layer-color — #1b1b1b
--tanrei-comp-menu-expanded-focused-supporting-text-color — #4b4b4b
--tanrei-comp-menu-expanded-focused-trailing-icon-color — #4b4b4b
--tanrei-comp-menu-expanded-focused-trailing-text-color — #4b4b4b
--tanrei-comp-menu-expanded-hovered-container-color — #1b1b1b · 8%
--tanrei-comp-menu-expanded-hovered-control-mark-color — #000000 · 0%
--tanrei-comp-menu-expanded-hovered-control-color — #000000 · 0%
--tanrei-comp-menu-expanded-hovered-label-color — #1b1b1b
--tanrei-comp-menu-expanded-hovered-leading-icon-color — #4b4b4b
--tanrei-comp-menu-expanded-hovered-mark-color — #000000 · 0%
--tanrei-comp-menu-expanded-hovered-state-layer-color — #1b1b1b
--tanrei-comp-menu-expanded-hovered-supporting-text-color — #4b4b4b
--tanrei-comp-menu-expanded-hovered-trailing-icon-color — #4b4b4b
--tanrei-comp-menu-expanded-hovered-trailing-text-color — #4b4b4b
--tanrei-comp-menu-expanded-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-menu-expanded-leading-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-expanded-mark-color — #000000 · 0%
--tanrei-comp-menu-expanded-pressed-container-color — #1b1b1b · 10%
--tanrei-comp-menu-expanded-pressed-control-mark-color — #000000 · 0%
--tanrei-comp-menu-expanded-pressed-control-color — #000000 · 0%
--tanrei-comp-menu-expanded-pressed-label-color — #1b1b1b
--tanrei-comp-menu-expanded-pressed-leading-icon-color — #4b4b4b
--tanrei-comp-menu-expanded-pressed-mark-color — #000000 · 0%
--tanrei-comp-menu-expanded-pressed-state-layer-color — #1b1b1b
--tanrei-comp-menu-expanded-pressed-supporting-text-color — #4b4b4b
--tanrei-comp-menu-expanded-pressed-trailing-icon-color — #4b4b4b
--tanrei-comp-menu-expanded-pressed-trailing-text-color — #4b4b4b
--tanrei-comp-menu-expanded-state-layer-color comp.menu.expanded.label.color #1b1b1b
--tanrei-comp-menu-expanded-supporting-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-expanded-trailing-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-expanded-trailing-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-focused-container-shape — 4px
--tanrei-comp-menu-focused-opacity — 1
--tanrei-comp-menu-group-title-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-group-title-padding-inline-compact comp.menu.container.padding-inline.compact 8px
--tanrei-comp-menu-group-title-padding-inline-regular comp.menu.container.padding-inline.regular 12px
--tanrei-comp-menu-group-gap sys.space.space200 16px
--tanrei-comp-menu-hovered-container-shape — 4px
--tanrei-comp-menu-hovered-opacity — 1
--tanrei-comp-menu-leading-icon-size-compact sys.icon.small 20px
--tanrei-comp-menu-leading-icon-size-regular sys.icon.medium 24px
--tanrei-comp-menu-mark-size-compact sys.icon.small 20px
--tanrei-comp-menu-mark-size-regular sys.icon.medium 24px
--tanrei-comp-menu-mark-width sys.border.thick 2px
--tanrei-comp-menu-motion-close-duration sys.motion.role.appear.anchored.exit 250ms
--tanrei-comp-menu-motion-close-easing sys.motion.easing.exit cubic-bezier(0.3, 0, 1, 1)
--tanrei-comp-menu-motion-feedback-duration sys.motion.role.feedback 450ms
--tanrei-comp-menu-motion-open-duration sys.motion.role.appear.anchored.enter 300ms
--tanrei-comp-menu-motion-open-easing sys.motion.easing.enter cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-menu-motion-state-duration sys.motion.role.state.large 250ms
--tanrei-comp-menu-motion-state-easing sys.motion.easing.state cubic-bezier(0.2, 0, 0, 1)
--tanrei-comp-menu-motion-submenu-delay sys.motion.duration.short4 200ms
--tanrei-comp-menu-pressed-container-shape — 4px
--tanrei-comp-menu-pressed-opacity — 1
--tanrei-comp-menu-radio-item-mark — 0
--tanrei-comp-menu-selected-container-color sys.color.secondary-container #262626 · 0%
--tanrei-comp-menu-selected-control-mark-color sys.color.on-primary #ffffff
--tanrei-comp-menu-selected-control-color sys.color.primary #1b1b1b
--tanrei-comp-menu-selected-disabled-container-color — #1b1b1b · 0%
--tanrei-comp-menu-selected-disabled-control-mark-color — #ffffff
--tanrei-comp-menu-selected-disabled-control-color — #1b1b1b · 38%
--tanrei-comp-menu-selected-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-menu-selected-disabled-leading-icon-color — #1b1b1b · 38%
--tanrei-comp-menu-selected-disabled-mark-color — #1b1b1b · 38%
--tanrei-comp-menu-selected-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-menu-selected-disabled-supporting-text-color — #1b1b1b · 38%
--tanrei-comp-menu-selected-disabled-trailing-icon-color — #1b1b1b · 38%
--tanrei-comp-menu-selected-disabled-trailing-text-color — #1b1b1b · 38%
--tanrei-comp-menu-selected-focused-container-color — #262626 · 10%
--tanrei-comp-menu-selected-focused-control-mark-color — #ffffff
--tanrei-comp-menu-selected-focused-control-color — #1b1b1b
--tanrei-comp-menu-selected-focused-label-color — #262626
--tanrei-comp-menu-selected-focused-leading-icon-color — #262626
--tanrei-comp-menu-selected-focused-mark-color — #1b1b1b
--tanrei-comp-menu-selected-focused-state-layer-color — #262626
--tanrei-comp-menu-selected-focused-supporting-text-color — #262626
--tanrei-comp-menu-selected-focused-trailing-icon-color — #262626
--tanrei-comp-menu-selected-focused-trailing-text-color — #262626
--tanrei-comp-menu-selected-hovered-container-color — #262626 · 8%
--tanrei-comp-menu-selected-hovered-control-mark-color — #ffffff
--tanrei-comp-menu-selected-hovered-control-color — #1b1b1b
--tanrei-comp-menu-selected-hovered-label-color — #262626
--tanrei-comp-menu-selected-hovered-leading-icon-color — #262626
--tanrei-comp-menu-selected-hovered-mark-color — #1b1b1b
--tanrei-comp-menu-selected-hovered-state-layer-color — #262626
--tanrei-comp-menu-selected-hovered-supporting-text-color — #262626
--tanrei-comp-menu-selected-hovered-trailing-icon-color — #262626
--tanrei-comp-menu-selected-hovered-trailing-text-color — #262626
--tanrei-comp-menu-selected-label-color sys.color.on-secondary-container #262626
--tanrei-comp-menu-selected-leading-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-menu-selected-mark-color sys.color.primary #1b1b1b
--tanrei-comp-menu-selected-pressed-container-color — #262626 · 10%
--tanrei-comp-menu-selected-pressed-control-mark-color — #ffffff
--tanrei-comp-menu-selected-pressed-control-color — #1b1b1b
--tanrei-comp-menu-selected-pressed-label-color — #262626
--tanrei-comp-menu-selected-pressed-leading-icon-color — #262626
--tanrei-comp-menu-selected-pressed-mark-color — #1b1b1b
--tanrei-comp-menu-selected-pressed-state-layer-color — #262626
--tanrei-comp-menu-selected-pressed-supporting-text-color — #262626
--tanrei-comp-menu-selected-pressed-trailing-icon-color — #262626
--tanrei-comp-menu-selected-pressed-trailing-text-color — #262626
--tanrei-comp-menu-selected-state-layer-color comp.menu.selected.label.color #262626
--tanrei-comp-menu-selected-supporting-text-color sys.color.on-secondary-container #262626
--tanrei-comp-menu-selected-trailing-icon-color sys.color.on-secondary-container #262626
--tanrei-comp-menu-selected-trailing-text-color sys.color.on-secondary-container #262626
--tanrei-comp-menu-separation sys.behavior.separation 0
--tanrei-comp-menu-shortcut-variant — 0
--tanrei-comp-menu-submenu-offset sys.space.space50 4px
--tanrei-comp-menu-surface-color sys.color.surface-bright #ffffff
--tanrei-comp-menu-surface-elevation sys.elevation.level2 0px 0px 0px 1px rgb(0 0 0 / 0.08), 0px 1px 2px 0px rgb(0 0 0 / 0.04)
--tanrei-comp-menu-surface-max-width — 320px
--tanrei-comp-menu-surface-min-width — 160px
--tanrei-comp-menu-surface-offset sys.space.space50 4px
--tanrei-comp-menu-surface-padding-block sys.space.space50 4px
--tanrei-comp-menu-surface-padding-inline sys.space.space50 4px
--tanrei-comp-menu-surface-shape sys.shape.corner.medium 8px
--tanrei-comp-menu-surface-viewport-margin sys.space.space100 8px
--tanrei-comp-menu-trailing-icon-size-compact sys.icon.small 20px
--tanrei-comp-menu-trailing-icon-size-regular sys.icon.medium 24px
--tanrei-comp-menu-unselected-container-color — #000000 · 0%
--tanrei-comp-menu-unselected-control-mark-color — #000000 · 0%
--tanrei-comp-menu-unselected-control-color sys.color.outline #848484
--tanrei-comp-menu-unselected-disabled-container-color — #000000 · 0%
--tanrei-comp-menu-unselected-disabled-control-mark-color — #1b1b1b · 38%
--tanrei-comp-menu-unselected-disabled-control-color — #1b1b1b · 38%
--tanrei-comp-menu-unselected-disabled-label-color — #1b1b1b · 38%
--tanrei-comp-menu-unselected-disabled-leading-icon-color — #1b1b1b · 38%
--tanrei-comp-menu-unselected-disabled-mark-color — #000000 · 0%
--tanrei-comp-menu-unselected-disabled-state-layer-color — #1b1b1b · 38%
--tanrei-comp-menu-unselected-disabled-supporting-text-color — #1b1b1b · 38%
--tanrei-comp-menu-unselected-disabled-trailing-icon-color — #1b1b1b · 38%
--tanrei-comp-menu-unselected-disabled-trailing-text-color — #1b1b1b · 38%
--tanrei-comp-menu-unselected-focused-container-color — #1b1b1b · 10%
--tanrei-comp-menu-unselected-focused-control-mark-color — #000000 · 0%
--tanrei-comp-menu-unselected-focused-control-color — #848484
--tanrei-comp-menu-unselected-focused-label-color — #1b1b1b
--tanrei-comp-menu-unselected-focused-leading-icon-color — #4b4b4b
--tanrei-comp-menu-unselected-focused-mark-color — #000000 · 0%
--tanrei-comp-menu-unselected-focused-state-layer-color — #1b1b1b
--tanrei-comp-menu-unselected-focused-supporting-text-color — #4b4b4b
--tanrei-comp-menu-unselected-focused-trailing-icon-color — #4b4b4b
--tanrei-comp-menu-unselected-focused-trailing-text-color — #4b4b4b
--tanrei-comp-menu-unselected-hovered-container-color — #1b1b1b · 8%
--tanrei-comp-menu-unselected-hovered-control-mark-color — #000000 · 0%
--tanrei-comp-menu-unselected-hovered-control-color — #848484
--tanrei-comp-menu-unselected-hovered-label-color — #1b1b1b
--tanrei-comp-menu-unselected-hovered-leading-icon-color — #4b4b4b
--tanrei-comp-menu-unselected-hovered-mark-color — #000000 · 0%
--tanrei-comp-menu-unselected-hovered-state-layer-color — #1b1b1b
--tanrei-comp-menu-unselected-hovered-supporting-text-color — #4b4b4b
--tanrei-comp-menu-unselected-hovered-trailing-icon-color — #4b4b4b
--tanrei-comp-menu-unselected-hovered-trailing-text-color — #4b4b4b
--tanrei-comp-menu-unselected-label-color sys.color.on-surface #1b1b1b
--tanrei-comp-menu-unselected-leading-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-unselected-mark-color — #000000 · 0%
--tanrei-comp-menu-unselected-pressed-container-color — #1b1b1b · 10%
--tanrei-comp-menu-unselected-pressed-control-mark-color — #000000 · 0%
--tanrei-comp-menu-unselected-pressed-control-color — #848484
--tanrei-comp-menu-unselected-pressed-label-color — #1b1b1b
--tanrei-comp-menu-unselected-pressed-leading-icon-color — #4b4b4b
--tanrei-comp-menu-unselected-pressed-mark-color — #000000 · 0%
--tanrei-comp-menu-unselected-pressed-state-layer-color — #1b1b1b
--tanrei-comp-menu-unselected-pressed-supporting-text-color — #4b4b4b
--tanrei-comp-menu-unselected-pressed-trailing-icon-color — #4b4b4b
--tanrei-comp-menu-unselected-pressed-trailing-text-color — #4b4b4b
--tanrei-comp-menu-unselected-state-layer-color comp.menu.unselected.label.color #1b1b1b
--tanrei-comp-menu-unselected-supporting-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-unselected-trailing-icon-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-unselected-trailing-text-color sys.color.on-surface-variant #4b4b4b
--tanrei-comp-menu-group-title-typescale sys.typescale.label-small 500 12px/1.3333
--tanrei-comp-menu-label-typescale sys.typescale.label 500 14px/1.4286
--tanrei-comp-menu-supporting-text-typescale sys.typescale.body-small 400 13px/1.5385
--tanrei-comp-menu-trailing-text-typescale sys.typescale.label-small 500 12px/1.3333

测试样例

作为基准的渲染结果。所有平台都以相同的 ID 为每个样例截取快照,以便相互比较。

生成自 specs/menu.spec.json, tokens/comp/menu.tokens.json, design/tokens/

指南

何时使用

菜单用于不需要在屏幕上占一席之地的次要操作和设置:行上的“更多”、工具栏的溢出菜单、排序方式、视图的选项。用户打开它,选一样东西,然后回到原先在做的事情上。

屏幕的主要操作请使用按钮,因为菜单会把它们藏起来。当选择本身就是屏幕的主要任务时,请使用列表、单选按钮或下拉选择框。当用户必须先作答才能继续时,请使用对话框。菜单只容纳它的项:不放输入框、滑块或开关,它们会占用菜单所需要的按键。

通向某个地方的行是链接项:给这一项一个 href。它适合一小组平时不占地方、需要时才唤出的去处,例如面包屑中折叠起来的层级、语言的选择,或“更多”。网站的主要页面始终可见,属于导航:请把它们放进导航栏、导航抽屉或由链接组成的列表。

如何选择项的种类

种类 它是什么 激活之后 用途
项 一个操作;带 href 时是链接 触发自己的 click,然后菜单关闭。链接项的目的地在这之后打开。 “复制”“重命名”“分享”。“删除”或“放弃”请设置 destructive:它会以错误颜色绘制。要通向某个地方时,给它一个 href。
复选项 一个可以独立打开或关闭的选项 切换自己的 checked,并触发自己的 change。 “显示网格”“显示标尺”:互不影响的设置。
单选项 它所在组的几个选项之一 设置所在组的 value,并由组触发 change。已选中的单选项什么也不触发。 “按名称排序”“按日期排序”:一个问题的一个答案。
子菜单 在旁边打开下一级的项 打开它的子菜单,并触发自己的 toggle。菜单保持打开。 “最近打开”“分享”“移动到”:另一组会让菜单变得太长的项。
组 一串相关的项,可以带标题 什么也不发生:它不接受焦点。 把相关的项与其他项分开,以及容纳同一个问题的各个单选项。

通过轻点、点击或 Enter 激活的复选项或单选项会关闭菜单,与操作一样。用 Space 激活时,菜单保持打开,这样键盘用户可以一次更改多项设置。

组、标题和分隔方式

  • 菜单会把一个组与它旁边的任何内容分开,但绝不在顶部或底部分隔:用一条分隔线,或者用一段间隙。选择间隙时,每个组以及组外每一串连续的项各自是一张独立的底板,底板之间是空白。你不需要自己放置分隔线或底板。
  • 用哪一种由设计系统选择,这是所有自行分隔部件的组件共用的一项选择:默认系统中是分隔线。separation 为单个菜单及其子菜单做决定:auto(默认)跟随设计系统,divider 或 gap 则指定其中一种,无论系统选的是什么。选择间隙时不暴露任何分隔符,而各个组仍然作为组暴露。
  • 带有 title 的组会把它画成各项上方的一个低调的标题,从行内容开始的位置画起,并以它命名。请给单选项所在的组一个提出问题的标题,例如“排序方式”。
  • 单选项应当放在组里。组外的单选项永远不会被选中,并会在调试构建中报告。
  • 组不能包含另一个组。内层组的项会被当作外层组的项来绘制。

标记列和图标列

复选项或单选项的标记位于行首的标记列中,前置图标位于其后的图标列中。iconSpace 决定哪些行要为自己不画任何内容的列保留空间:

  • auto(默认)按分区决定:分区是一个组,或组外一串相邻的项。只要同一分区的另一项在某列中画了内容,这一行就保留该列的空间,因此一个分区的标签从同一条线开始。没有标记也没有图标的分区,标签从行内容开始的位置起画,前面不留空白。
  • always 在每一行都保留这两列,因此菜单的所有标签都从同一条线开始。
  • never 只给在该列中画了内容的行设这一列,因此每个标签紧跟在它自己的标记或图标之后开始。

每个表面各自决定,子菜单跟随其菜单的 iconSpace。无论组内各行保留什么,组的标题都从行内容开始的位置起画。树视图和导航抽屉也有同样的 iconSpace,取值和默认值都相同。

密度

density 决定一行占多大空间。regular(默认:行高 48,图标 24)适用于既用触摸也用指针的产品。compact(行高 32,图标 20,即树视图的紧凑行)适用于用鼠标操作、菜单很长且经常打开的工具。两者的文字大小相同。触摸时行高绝不低于触摸目标,所以手机上的紧凑菜单行高仍是 48。每个子菜单都采用其菜单的密度。

链接

链接项是平台自己的链接,所以浏览器的上下文菜单、在新标签页中打开,以及悬停时显示的地址都照常可用。它没有自己的外观:在每一种状态下,它都画得与相邻行的操作一样。

  • 激活。 点击、轻点、Enter 或 Space 会触发该项的 click,然后菜单关闭,然后目的地打开。Space 会激活链接项,就像它激活菜单的其他每一行一样。在别处,Space 不会激活链接。
  • 路由。 取消该项的 click,就可以自己接管导航。菜单仍会关闭,其他什么也不会打开。
  • 其他标签页。 带修饰键的点击或 Enter,以及中键点击,交给浏览器处理。菜单不触发 close,并保持打开,所以可以把其中的几个链接一个接一个地打开。
  • 新上下文。 target: new 打开新的标签页或窗口。该项在后置图标列中以图标集的 open_in_new 结尾,除非 trailingIcon 指定了你自己的字形;targetLabel 则把这一点告诉辅助技术。快捷键或后置文本保留自己的那一列,位于这个标记之前。
  • 当前所在的页面。 current 标出用户当前所在页面的链接项。它作为当前页面暴露(在 Web 上是 aria-current="page"),在每种对比度方案下都以自己的填充绘制,不画对勾,标签也不加粗:它说明用户身在何处,而不是用户做出的选择。它由你来设置,菜单从不设置。没有 href 时它会被忽略。
  • 禁用。 禁用的链接项无论用什么方式都无法打开。与任何禁用的项一样,它仍然可以用方向键到达。
  • 只限“项”这一种。 复选项、单选项和子菜单项不接受目的地:它们持有一个状态,或者打开下一级。

子菜单

子菜单项是一个以图标集的 chevron_right 结尾的行,它的子项就是下一级:任何种类的项,包括组。它不是操作,也没有快捷键。

  • 打开。 按下、轻点、Enter、Space,或者指向行末方向的方向键,会立即打开子菜单,并把焦点移到其中第一个启用的项。指针停留在该项上时,会在短暂延迟后打开子菜单,而焦点留在该项上。
  • 前往子菜单的途中。 指针从该项移向它已打开的子菜单时,途中经过的项既不显示悬停,也不获得焦点,所以沿斜线移动不会关闭子菜单。
  • 关闭一级。 指向行首方向的方向键或 Escape,会关闭持有焦点的那一级,并把焦点交还给打开它的项。只有在没有子菜单打开时,Escape 才关闭整个菜单。移到父级的另一项上也会关闭子菜单。一个表面中最多只有一个子菜单处于打开状态。
  • 全部关闭。 任何一级中的操作、Tab、在所有表面之外按下,以及焦点离开所有表面,都会关闭整个菜单,并且只触发一次 close。
  • 放在哪里。 放在容纳该项的表面旁边,位于末尾侧,它的第一行与该项所在的行齐平;末尾侧没有空间时放在另一侧。窗口太窄、两侧都放不下时,它叠放在父级之上,并让父级露出一条窄边:在这条窄边上按下会关闭子菜单。菜单绝不会用子菜单的内容替换自己的内容。
  • open 与 toggle。 子菜单项的 open 属于它自己。用户打开或关闭子菜单时,由菜单设置它,并由该项触发 toggle。你自己对 open 的更改,以及整个菜单的关闭,都不触发 toggle。
  • 深度。 菜单之下的子菜单请不要超过两级。第三级仍然会画出来,并会在调试构建中报告;没有项的子菜单也会被报告。
  • 组的 value 只作用于它自己所在表面中的单选项,每个表面各自决定自己的标记列和图标列。

上下文菜单

上下文菜单在用户指向的位置,给出适用于所指对象的操作。它仍是同一个菜单,项、按键和子菜单都一样,只是在区域中的某个位置打开,而不是从按钮打开。

  • 区域。 把 trigger 设为 context,把 anchor 设为区域的 id:画布、列表、表格的一行、文档。区域不是按钮,因此菜单不会在它上面写入弹出状态。
  • 如何请求。 右键点击(在 Mac 上也可以按住 Control 点击或双指点击)、用触摸或触控笔按住不动半秒、在有焦点的元素上按 Shift+F10 或 ContextMenu 键,或使用屏幕阅读器的上下文菜单命令。菜单发出 contextRequest,带有位置、所针对的元素和请求方式:pointer、long-press 或 keyboard。和从锚点打开时一样,由你设置 open。
  • 在哪里打开。 以一个角对准该位置,中间不留间隙:默认在位置下方、朝行尾方向展开,可用 placement 指定。靠近窗口边缘时,它会在各个轴上分别翻到位置的另一侧,因此不会盖住该位置。页面滚动时,它随区域一起移动。用按键请求时,它像对锚点那样,贴着有焦点的元素打开。
  • 浏览器自己的菜单。 菜单每响应一次请求,就取消浏览器的上下文菜单。对于你的菜单不处理的地方,比如输入框、链接或选中的文本,取消 contextRequest 就能把请求留给浏览器。
  • 焦点和关闭。 焦点移到第一个可用的项;关闭时,焦点回到请求菜单时拥有焦点的元素。在菜单之外按下的任何地方都会关闭它,在区域内也一样;在别处右键点击会先关闭它,再在那里重新请求。
  • 放什么。 只放适用于所指对象的操作,最常用的放在前面。每个操作也要在产品的其他地方提供:不知道如何请求的人永远看不到上下文菜单。
  • 在 iOS 上,长按也可能选中下面的文本。如果区域中的文本不应被选中,请在它的样式中说明(user-select: none、-webkit-touch-callout: none)。

打开、焦点和关闭

菜单是供操作的浮层,但不是模态的。用户在其中进行操作,所以它会取得焦点,再把焦点交还回去。屏幕上的其他内容都不会被阻挡。

  • 打开。 锚点被激活时,由你设置 open。表面出现在平台的浮层中,按 placement 所说的方式贴着锚点放置:默认在锚点下方,起始侧的边缘对齐。这一侧没有空间时,它翻到另一侧;它会平移以留在屏幕之内,太高时可以滚动,并在锚点移动时跟随锚点。
  • 焦点进入。 焦点移到第一个启用的项。此后方向键、Home、End 和键入查找在各项之间移动,到头时循环,并且穿过各个组。指针移到某一项上时,焦点也移到那里,所以按键会从指针所在的位置继续。
  • 非模态。 菜单不把焦点限制在自己之内,不让屏幕的其余部分变为惰性(inert),也不画遮罩。Tab 会离开菜单并关闭它。
  • 关闭。 菜单会请求关闭,并说明原因。这些原因在每个平台上名称相同,只是大小写遵循各平台自己的写法:
原因 何时
escape 按下了 Escape。
back 平台的返回操作,例如 Android 的返回按钮或返回手势。
outside 在表面和锚点之外按下。对上下文菜单来说,是表面之外的任何地方,区域内也算。
tab 按下了 Tab 或 Shift+Tab。焦点随后从锚点继续前进,就像原本从锚点出发一样。对上下文菜单来说,焦点从请求菜单时拥有焦点的元素继续前进。
focus-out 焦点以其他方式移出了菜单。
item 某一项被激活。
  • 焦点返回。 菜单关闭时,如果焦点原本在菜单之内,就把焦点交还给锚点。上下文菜单会把焦点还给请求菜单时拥有焦点的元素;如果当时没有元素拥有焦点,焦点只是离开菜单。

由谁来关闭菜单,取决于平台。在 Web 和 React Native 上,你在 close 事件中把 open(visible)设回 false。在 Compose 上,你在 onDismissRequest 中把 expanded 设为 false,它带有一个 TanreiMenuCloseReason。在 Flutter 上,菜单带着原因调用 onClose,并通过它的控制器自行关闭。在 SwiftUI 上,菜单向 isPresented 写入 false,所以原因不会传递出来。

在外部的按下是否也会传到它下方的内容,同样取决于平台。在 Web 和 Flutter 上会。在 SwiftUI、Compose 和 React Native 上,这次按下只关闭菜单。

推荐

  • 标签保持简短,操作的标签以动词开头:“复制链接”“删除文件”。
  • 操作的键盘快捷键写在 shortcut 里,使用与平台无关的按键名称(mod+c),并在你的产品中绑定这些按键。trailingText 留给其他内容:后置文本绝不会作为快捷键暴露。
  • 把破坏性操作放在最后,让它单独成组,或放在一个组之后,与其他项分开。
  • 有时可用的项被禁用时,让它留在原位,这样菜单的形状不会改变。用户仍然可以用方向键到达它。

避免

  • 不要把屏幕的主要操作放进菜单。
  • 不要把复选项用于互斥的选择。请使用组里的单选项。
  • 不要做只有一项的菜单。把这个操作直接显示为按钮。
  • 不要把用户常用的操作放进子菜单,子菜单也不要超过两级。
  • 不要在操作上使用后置的 chevron_right。它是子菜单项的标记。
  • 不要把网站的主导航做成由链接组成的菜单。菜单是唤出来的,用完就消失。

无障碍

角色
menu
标注
The menu is named by its anchor (a context menu by the name the product gives it, and by none where it gives none), and a submenu by its submenu item's label. Each item's label is its name and its supporting text its description; its shortcut is exposed as the item's keyboard shortcut where the platform has one, and otherwise given as text in its description (KBS-10); trailing text is description only. A link item is a menu item like any other, named by its label; while it opens a new context its targetLabel ends its description. Checkbox and radio items expose their checked state, whatever their mark looks like. A submenu item exposes that it opens a menu and whether that menu is open. A group is named by its title. Icons, the mark and the submenu indicator are decorative.
最小目标
即使外观尺寸更小,点击区域也满足最小目标尺寸。 minimum 32px · pointer 44px · spacing 8px · touch 48px

键盘操作

按键 动作
Enter, Space / Down Arrow on the anchor The product opens the menu; focus moves to the first enabled item.
Shift+F10 / the ContextMenu key on the area / an element inside it (trigger context) The menu asks to be opened against that element (contextRequest); the product opens it, and focus moves to the first enabled item.
Down Arrow / Up Arrow Moves focus to the next or previous item of the level that holds focus, wrapping at the ends.
Home / End Moves focus to the first or last item of that level.
Printable characters Moves focus to the next item of that level whose label begins with the characters typed.
Right Arrow (Left Arrow in a right-to-left layout) on a submenu item Opens the submenu and moves focus to its first enabled item.
Left Arrow (Right Arrow in a right-to-left layout) in a submenu Closes that submenu and returns focus to its submenu item.
Enter Activates the focused item and closes the menu and its submenus, a link item's destination opening after that; on a submenu item, opens the submenu instead.
Space Activates the focused item, a link item as Enter does; a checkbox or radio item leaves the menu open; on a submenu item, opens the submenu.
Enter with a modifier key, on a link item (the web) What the browser does with a link: another tab or window, a download. The menu emits no close and stays open, with focus on the item (MNU-54).
Escape Closes the innermost open submenu and returns focus to its submenu item; with no submenu open, closes the menu and returns focus to the anchor (a context menu: to the element that had focus when it was asked for).
Tab / Shift+Tab Closes the menu and every submenu, and moves focus on from the anchor.

备注

  • The anchor is the product's own button, icon-only Button or field. The menu does not draw it; it reads its position, returns focus to it, and sets its expanded state where the platform has one (aria-haspopup and aria-expanded on the web).
  • A context menu (trigger context) is the same menu opened at a point of an area instead of from a button: a secondary click, a long press, Shift+F10 or the ContextMenu key, as the WAI-ARIA menu pattern names for a context-specific menu. The area is not a button, so it carries no popup state; the menu cancels the platform's own context menu only where it answers the request, and a product that cancels contextRequest — over a field, a link, selected text it does not handle — leaves the platform's menu in place. Every action a context menu offers is also reachable elsewhere in the product, because a context menu is found only by those who know to ask for it (MNU-60 to MNU-63).
  • Five item kinds: item (an action), checkbox-item, radio-item, submenu and group. A checkbox item's checked is its own; which radio item is checked is its group's, by value; a submenu item's open is its own. A group may hold items of any kind except another group; a submenu may hold items of every kind.
  • A link item is the platform's own link exposed as a menu item: on the web an anchor with the menu item role. Assistive technology reads a menu by its items — how many, which one, the arrow keys — and that reading holds only while every child of the menu is a menu item; so the role stays, and what makes the row a link stays underneath it: its address, the browser's context menu and new-tab gestures, a crawler that follows it. It is the one link form that Space activates, because it is operated as a menu item and looks like the action beside it. A menu of nothing but links to a site's main pages, always in view, is navigation and belongs in a navigation landmark of links; the menu is for a short set that is out of the way until asked for (the folded levels of a trail, a choice of language, "more").
  • Focus: the menu is an operated overlay that is not modal. It moves focus in when it opens and returns it to the anchor when it closes, but does not keep it: Tab leaves the menu and closes it, at whatever level focus is. A submenu takes focus when it is opened by a key or a press and returns it to its submenu item when it closes alone.
  • Submenus follow the WAI-ARIA menu pattern: the arrow key that points to where the submenu opens, Enter and Space open it and move focus in; the opposite arrow key and Escape close it and return focus to its item; type-ahead, Home, End and the vertical arrow keys work inside the level that holds focus. A pointer opens a submenu by resting on its item, and may then cross other items on its way to the submenu without closing it (MNU-45); touch opens it with a tap. In a window too narrow to set two surfaces side by side the submenu is laid over its parent, never swapped in for it, so the keys, the roles and the reading order are the same at every size (MNU-47).
  • Depth: one level of submenus where possible, never more than two. Each level costs a person a precise sideways movement and hides the level before it on a small screen; a set that needs more belongs in a dialog, a sheet or a page. A debug build reports a third level; nothing stops it from working (MNU-52).
  • A checked item is told from an unchecked one by its mark, which differs in shape (an empty and a filled box, a thin and a heavy ring, a check or none), never by the row's fill alone: the fill supports the mark and is not held to a contrast of its own. It is still meant to be seen: in the default system a checked row's fill is a layer of the row's content color over the sheet, so it differs from the sheet and from a hovered row by the same step in every scheme (MNU-37), and the content on it keeps at least 4.5:1 in every state. The control looks are drawings in the mark column; the menu holds no checkbox and no radio button.
  • Under gap no separator is exposed between groups; the groups themselves, and their titles, carry the structure to assistive technology, as they do under divider.
  • A menu holds only its items. A field, slider or switch inside a menu would take arrow keys and typing that the menu needs.
  • Text size does not clip an item: items grow at 200% text size, and the surface scrolls when it no longer fits the visible part of the window.
  • An item's shortcut is drawn by the keyboard shortcut component (KBS-13), which writes the keys for the keyboard the person uses and only shows them: the product binds them. Trailing text is never read as a shortcut; a product that typed its shortcut there keeps the text but not the keyboard shortcut semantics until it moves the keys to shortcut.
  • 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.
  • WCAG 2.5.8 (target size): for touch every row is at least the touch target high in both densities (MNU-12). The compact density is lower than the pointer target for a fine pointer, by design: it is the density of a tool used with a mouse, it stays above the 24 by 24 the criterion asks, and its rows reach across the surface's whole width. A product that cannot accept that uses regular.

生成自 specs/menu.spec.json, tokens/sys/measure.tokens.json

组件负责的部分

  • 表面作为菜单暴露。操作是菜单项,复选项是菜单项复选框,单选项是菜单项单选按钮,各自带有选中状态、自己的位置和菜单的大小。
  • 每一项以它的标签命名,以它的辅助文本作为描述。平台有相应属性时(Web 上是 aria-keyshortcuts),项的 shortcut 作为它的键盘快捷键暴露;否则,逐个拼出的按键是它描述的一部分。后置文本只会是描述的一部分。图标和标记是隐藏的:方框、圆环和对勾只是图形,选中状态属于项本身。
  • 链接项作为菜单项暴露,而不是作为链接:菜单是按它的项、项的数量和方向键来朗读的,而这只有在每一行都是菜单项时才成立。在底层,它仍然是平台的链接。它会打开新上下文时,targetLabel 位于它描述的末尾,它的标记是隐藏的。当前链接项还会作为当前页面暴露。
  • 组作为以其标题命名的组暴露。分隔线是不接受焦点的分隔符;用间隙分开各组时,没有分隔符。标题不接受焦点,也不会被当作项来朗读。
  • 子菜单项作为一个会打开菜单的项暴露,并带有展开状态,被禁用时也是如此。它的表面是一个独立的菜单,以该项的标签命名,每个表面各自计算自己的位置。
  • 平台有相应属性时(在 Web 上是 aria-haspopup、aria-controls 和 aria-expanded),锚点会被告知它会打开一个菜单,以及菜单是否打开。
  • 上下文菜单不改动它的区域:区域不是按钮,也不会被告知它会打开菜单。菜单通过右键点击、长按、Shift+F10、ContextMenu 键或辅助技术的上下文菜单命令打开,名称只来自你提供的 aria-label,就像平台自己的上下文菜单也没有名称一样。
  • 打开时焦点移到第一个启用的项。关闭时,如果焦点原本在菜单之内,就把焦点交还给锚点。上、下方向键到头时循环,Home 和 End 前往两端,键入字符会移到标签以所键入字母开头的下一项。Enter 激活并关闭。在复选项或单选项上按 Space,菜单保持打开。Escape 关闭菜单并交还焦点。Tab 关闭菜单,焦点从锚点继续前进。在子菜单中,指向行首方向的方向键和 Escape 只关闭那一级,并把焦点交还给打开它的项;沿行方向而不起作用的方向键,绝不会传到菜单后面的页面。
  • 禁用的项仍然可以用方向键到达,但不会被激活。
  • 焦点环画在项的内部,所以表面不会把它裁掉。它只在键盘聚焦时显示。指针移到某一项上时,该项显示为悬停,而不是聚焦。
  • 每一项的高度不小于最小目标,整行都是它的点击区域。
  • 文本变大时,项随之增高;表面在屏幕上放不下时可以滚动。
  • 图标字体还无法画出某个图标标记(例如对勾)时,菜单在它的位置上什么也不画,绝不会画出“check”这个单词。它保留这一列的宽度,仍然暴露选中状态,并在字体加载完成后显示标记。
  • 调试构建会报告:没有标签的项、组外的单选项、没有指向组内任何单选项的组值、两个值相同的单选项、组内的组、同时有快捷键和后置文本的项、菜单没有定义的 separation 或 iconSpace、第三级子菜单,以及没有项的子菜单。它们还会报告:会打开新上下文却没有 targetLabel 的链接项;复选项、单选项或子菜单项上的 href;指向脚本或无法解析的 href;菜单没有定义的 target;以及没有 href 的项上的 current。菜单未定义的 trigger 也会被报告。

已知的平台差距:

  • SwiftUI、Compose、Flutter 和 React Native 没有链接项:在这些平台上,项还不接受 href。
  • SwiftUI、Compose、Flutter 和 React Native 没有上下文触发方式:它们的菜单只能从锚点打开。
  • SwiftUI、Compose、Flutter 和 React Native 没有子菜单、separation、iconSpace 和 markIcon,选中的行也没有填充。只要菜单中有任何一项在标记列或图标列中画了内容,它们就在每一行都保留该列。它们仍然在 16 的方框里用各自的标记来画对勾,组的标题从标签开始的位置画起。
  • 在 m3-like 和 cupertino 下,标题从哪里开始遵循的是 Tanrei 的规则,还没有对照原版测量过。
  • Web:菜单不监听平台的关闭请求,例如 Android 的返回手势,所以从不发送 back。
  • Web:aria-haspopup 和 aria-expanded 设置在锚点上。锚点是自定义元素(例如只有图标的 <tanrei-button>)时,它们设置在其 shadow root 内的按钮上。aria-controls 以及来自锚点的菜单名称无法跨越 shadow root。
  • Web:表面由脚本定位,而不是用 CSS 锚点定位。
  • Web:子菜单项无法用 aria-controls 指向它的表面,因为 aria-controls 无法伸入 shadow root。作为替代,表面在无障碍树中是该项的后代。
  • Web,macOS 上的 WebKit:子菜单已打开的项在按下时保持悬停颜色,而不是换成按下颜色。
  • SwiftUI 没有菜单、菜单项或分隔符角色,没有快捷键属性,也无法表明锚点会打开菜单或菜单已经打开。每一项是一个按钮,把位置作为自己的值(“2 of 3”,只有英文),复选项是一个切换开关,单选项在选中时处于 selected 状态。
  • SwiftUI 从不把键盘焦点交给禁用的按钮,所以方向键会跳过禁用的项,而不是到达它。
  • SwiftUI 把菜单呈现为 popover,而 popover 会自行定位。令牌只能选择位于哪一侧:popover 与锚点、与窗口边缘的距离由它自己决定,对齐由它自己完成,关闭时用的也是它自己的动效。在 iOS 26 上,它把表面裁剪成自己更大的圆角,并画出自己的阴影。这在设备上看起来如何,还没有检查过。
  • SwiftUI:Tab 会关闭菜单并把焦点交还给锚点,但焦点无法在同一次按键中从锚点继续前进。
  • SwiftUI 只从直接加在 TanreiMenuOption 上的 .tag(_:) 读取单选项的值。
  • SwiftUI:方向键、Home、End、键入查找、Escape 和 Tab 是作为规则来测试的,还没有用真实键盘测试。
  • React Native 把菜单画在 TanreiProvider 的浮层中,所以它的上层必须有一个 TanreiProvider。Tab 会关闭菜单并把焦点交还给锚点,但无法在同一次按键中从锚点继续前进。下一次 Tab 才会继续前进。
  • React Native:屏幕阅读器如何对待打开的菜单后面的元素,以及焦点离开菜单时的情况,还没有在设备上检查过。
  • React Native 没有菜单项复选框或菜单项单选按钮角色,也没有快捷键属性。每一项都是菜单项,复选项和单选项带有选中状态,后置文本作为描述的一部分朗读。
  • React Native 无法在你的锚点上设置属性。请自己给它 accessibilityState={{ expanded: open }}。
  • React Native 不会跟随在菜单打开期间移动的锚点。它在菜单打开时和窗口大小改变时测量锚点。
  • React Native:方向键、键入查找、Escape、Tab,以及 Space 与 Enter 的区别,还没有在带硬件键盘的设备上检查过。
  • Compose 没有菜单、菜单项或分隔符角色,也没有快捷键属性。表面是一个集合,操作没有角色,复选项是复选框,单选项是单选按钮,后置文本作为描述的一部分朗读。菜单无法设置锚点的展开状态。
  • Compose:菜单打开期间在锚点上的按下,算作在外部的按下。它以 outside 关闭菜单,并且不会传到锚点。
  • Compose:弹出层的窗口焦点、真实的外部按下、返回按钮、焦点交还给锚点、Tab 从锚点继续前进,以及 TalkBack 在菜单上的表现,只在 Robolectric 下测试过,还没有在设备上测试。
  • Flutter 没有组或分隔符角色,无法用锚点为菜单命名,也无法表明锚点会打开菜单,并且不给项提供位置或集合大小。组是一个以其标题命名的容器。锚点确实会得到展开状态。
  • Flutter:TalkBack、VoiceOver 和 Web 屏幕阅读器对菜单朗读什么,以及 Android 的返回手势在打开的菜单上的表现,还没有在设备上检查过。

需要你做的事

  • 通过锚点为菜单命名。给作为锚点的图标按钮一个标签,例如“更多”或“排序”。在 React Native 上,把同一个名称作为菜单的 accessibilityLabel 传入。
  • 在 Compose 和 React Native 上,在锚点上暴露菜单是否打开。菜单无法替你在那里设置。
  • 菜单请求关闭时就关闭它,并且在它关闭时不要自己移动焦点。菜单会把焦点交还给锚点。
  • 用 aria-label 为上下文菜单命名,比如“画布”,并在产品的其他地方也提供它的每个操作。
  • 给每一项一个标签,包括显示图标的项。标签就是名称,也是键入查找所匹配的内容。
  • 给单选项所在的组加一个标题,说明这项选择是关于什么的。
  • 给每个子菜单项一个标签:它既为该项命名,也为它打开的子菜单命名。
  • 给在新标签页中打开的链接项一个用你的产品语言写的 targetLabel,例如“在新标签页中打开”。Tanrei 不自带任何文字。