Skip to the page
TACTILE UI

Kit part · Dropdowns & menus · Popovers

Menu

A card of actions and options that slides out from under its key. The key stays down while the card is out; an option that is on lights its lamp.

Element
role="menu"
Needs
kit.css + kit.mjs
Rules
menu.rules.md · 6
Finishes
Aluminium · Graphite · Porcelain · Black
Volumes
Tactile · Quiet
On this page
  1. Variants
  2. Every state, at once
  3. API
  4. Markup
  5. Accessibility
  6. How it’s built
  7. Rules
  8. Related parts
01

Variants

Actions with shortcuts
Options · lamps
From an icon key
A line with a submenu
02

Every state, at once

Both finishes, side by side. Hover, pressed and focus are forced with .is-hover, .is-down and .is-focus; checked and disabled are real.

Aluminium

PartRestUnder the fingerOn · card outUnavailableFocus
Key
Line

Graphite

PartRestUnder the fingerOn · card outUnavailableFocus
Key
Line
03

API

NameKindValuesWhat it does
.tui-menu__triggerclasson a .tui-keyThe key: prints a small arrow and stays latched while its card is out.
popovertarget · aria-haspopup="menu"attributeon the keyNative opening and light dismiss; the part adds the keyboard and sets aria-expanded.
.tui-menuclasspopover role="menu"The card: a raised plate, 11px corners, 5px inside. Without popover it sits in the page, for specimens.
role="menuitem"attributeon <button>An action. Choosing it closes every card and returns focus to the key.
role="menuitemcheckbox" · aria-checkedattribute"true" · "false"An option with a green lamp. Toggles and the card stays.
role="menuitemradio" · aria-checkedattributein role="group"One of a set, with an amber lamp. Choosing it clears the others in its group.
aria-disabled="true"attributeon a lineShown at 45%, still reachable by keyboard, does nothing.
kbdelementlast in a lineThe shortcut, printed at the right in the second ink.
svgelementfirst in a lineA 14px icon in the lamp slot. Optional.
role="separator" · .tui-menu__labelattribute · class—A printed rule between groups; a printed label over a group.
.tui-menu__item--dangerclasson a lineRed printing, for erasing and removing. Confirm in a dialog.
aria-haspopup="menu" + popovertargetattributeon a lineA submenu: its .tui-menu goes inside the parent card. It hangs to the right, level with the line.
data-menu-contextattributea menu idOn any element: right-click, Shift+F10 or the Menu key opens that card there.
openMenu(menu, { anchor, focus }) · closeMenu(menu)functionkit.mjsOpen beside an element or a point {x, y}; focus "first", "last", "menu" or "none".
Enter · Space · ↓ · ↑keyboardon the keyOpen and go to the first line; ↑ goes to the last.
↑ ↓ · Home · End · a letterkeyboardin the cardMove, wrapping at the ends; a letter jumps to the next line starting with it.
→ · ←keyboardin the cardOpen a submenu and go into it; close it and go back to its line.
Escape · Tabkeyboardin the cardEscape closes one card and focus returns; Tab closes them all and moves on from the key.
04

Markup

<button class="tui-key tui-menu__trigger" popovertarget="reel-menu"
        aria-haspopup="menu" aria-expanded="false">Reel 4</button>

<div class="tui-menu" id="reel-menu" popover role="menu" aria-label="Reel 4">
  <button role="menuitem" type="button" tabindex="-1">Rename<kbd>F2</kbd></button>
  <button role="menuitem" type="button" tabindex="-1">Duplicate<kbd>Ctrl D</kbd></button>
  <div role="separator"></div>
  <button role="menuitemcheckbox" aria-checked="true" type="button" tabindex="-1">Loop playback</button>
  <div role="group" aria-label="Sort takes">
    <span class="tui-menu__label" aria-hidden="true">Sort takes</span>
    <button role="menuitemradio" aria-checked="true" type="button" tabindex="-1">By time</button>
    <button role="menuitemradio" aria-checked="false" type="button" tabindex="-1">By name</button>
  </div>
  <div role="separator"></div>
  <button class="tui-menu__item--danger" role="menuitem" type="button" tabindex="-1">Erase reel<kbd>Del</kbd></button>
</div>

<!-- The same card as a context menu: -->
<div tabindex="0" data-menu-context="reel-menu">Take 12</div>
import '/kit/kit.mjs';   // wires every menu, now and later

document.getElementById('reel-menu').addEventListener('click', (e) => {
  const line = e.target.closest('[role^="menuitem"]');
  if (!line) return;
  // aria-checked is already updated when this runs.
  console.log(line.textContent, line.getAttribute('aria-checked'));
});

import { openMenu, closeMenu } from '/kit/kit.mjs';
openMenu(menu, { anchor: { x: 200, y: 120 }, focus: 'first' });

Needs kit.css and kit.mjs. How to install the kit.

05

Accessibility

  • The WAI-ARIA menu button pattern: the key has aria-haspopup="menu" and an aria-expanded the part keeps true while the card is out.
  • Focus moves into the card and returns to the key when it closes: by Escape, by choosing a line, or by clicking elsewhere with nothing else taking focus.
  • An option’s state is aria-checked; the lamp is the second signal. Unavailable lines stay in the order with aria-disabled, so nothing jumps.
  • Hovering a line focuses it, so the pointer and the keyboard never disagree about which line is chosen.
  • A context menu must also open from the keyboard: give the area tabindex="0" and say “Shift F10” in its label.
06

How it’s built

The browser’s own popover does the opening, the top layer and light dismiss, so the card is never clipped by a panel. menu.mjs adds the menu pattern on top by delegation from the document, so cards added later work too: arrow keys, type-ahead, submenus, checkable lines, and focus going home. popover.mjs places the card under its key and flips it when the page runs out. The line under the finger is the kit’s recess; the lamps are the kit’s lamps, drawn as each line’s ::before. The card slides 6px out of the key in 150ms.

07

Rules

menu.rules.mdIn the brain

Actions read as verbs, options carry a lamp, and the key that opened the card is where focus goes back.

6 rules in this file, with the foundations they rest on and the anti-patterns that break them. Full rules ship in the brain with the Pass.

08

Reserve

Kept for a power cut.

Popover

A card hung from the key that opened it, with a printed leader running back to the key. Not modal: the page stays live around it.

Key

Every button, and every link that acts like one: a cap on a skirt that drops below the plate when pressed.

2 matches
BatteryGB
Set battery reserve

Command palette

Every action and page behind one key: a modal plate whose entry line is a lit display, with the results printed below and the chosen one pressed in.

Docs
Finish
Volume