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.
Kit part · Dropdowns & menus · Popovers
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.
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
| Part | Rest | Under the finger | On · card out | Unavailable | Focus |
|---|---|---|---|---|---|
| Key | |||||
| Line |
Graphite
| Part | Rest | Under the finger | On · card out | Unavailable | Focus |
|---|---|---|---|---|---|
| Key | |||||
| Line |
| Name | Kind | Values | What it does |
|---|---|---|---|
| .tui-menu__trigger | class | on a .tui-key | The key: prints a small arrow and stays latched while its card is out. |
| popovertarget · aria-haspopup="menu" | attribute | on the key | Native opening and light dismiss; the part adds the keyboard and sets aria-expanded. |
| .tui-menu | class | popover role="menu" | The card: a raised plate, 11px corners, 5px inside. Without popover it sits in the page, for specimens. |
| role="menuitem" | attribute | on <button> | An action. Choosing it closes every card and returns focus to the key. |
| role="menuitemcheckbox" · aria-checked | attribute | "true" · "false" | An option with a green lamp. Toggles and the card stays. |
| role="menuitemradio" · aria-checked | attribute | in role="group" | One of a set, with an amber lamp. Choosing it clears the others in its group. |
| aria-disabled="true" | attribute | on a line | Shown at 45%, still reachable by keyboard, does nothing. |
| kbd | element | last in a line | The shortcut, printed at the right in the second ink. |
| svg | element | first in a line | A 14px icon in the lamp slot. Optional. |
| role="separator" · .tui-menu__label | attribute · class | — | A printed rule between groups; a printed label over a group. |
| .tui-menu__item--danger | class | on a line | Red printing, for erasing and removing. Confirm in a dialog. |
| aria-haspopup="menu" + popovertarget | attribute | on a line | A submenu: its .tui-menu goes inside the parent card. It hangs to the right, level with the line. |
| data-menu-context | attribute | a menu id | On any element: right-click, Shift+F10 or the Menu key opens that card there. |
| openMenu(menu, { anchor, focus }) · closeMenu(menu) | function | kit.mjs | Open beside an element or a point {x, y}; focus "first", "last", "menu" or "none". |
| Enter · Space · ↓ · ↑ | keyboard | on the key | Open and go to the first line; ↑ goes to the last. |
| ↑ ↓ · Home · End · a letter | keyboard | in the card | Move, wrapping at the ends; a letter jumps to the next line starting with it. |
| → · ← | keyboard | in the card | Open a submenu and go into it; close it and go back to its line. |
| Escape · Tab | keyboard | in the card | Escape closes one card and focus returns; Tab closes them all and moves on from the key. |
<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.
aria-haspopup="menu" and an aria-expanded the part keeps true while the card is out.aria-checked; the lamp is the second signal. Unavailable lines stay in the order with aria-disabled, so nothing jumps.tabindex="0" and say “Shift F10” in its label.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.
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.
Reserve
Kept for a power cut.
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.
Every button, and every link that acts like one: a cap on a skirt that drops below the plate when pressed.
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.