Menus & popovers
A menu is a short list of commands raised by a control. A context menu is the same list raised by pressing and holding the thing it acts on. A popover is a panel anchored to a control, holding content rather than commands.
When to use each
Use a menu when a button offers several related commands: a More button, a sort control, a pull-down.
Use a context menu when the commands belong to a specific item and would clutter the screen if shown all the time. Long press a message, a file, a photo.
Use a popover when the thing you are showing has content in it rather than a list of verbs: a form, a colour picker, a small detail panel. On iPhone, consider a sheet instead; popovers are mostly an iPad shape.
Use an action sheet when the choices are consequential enough to deserve the bottom of the screen.
Menus
<div class="p-menu p-menu-touch" role="menu">
<button class="p-menu__item" role="menuitem" type="button">
<i class="p-menu__icon ph-bold ph-copy" aria-hidden="true"></i>
<span>Copy</span>
<span class="p-menu__shortcut">⌘C</span>
</button>
<div class="p-menu__separator" role="separator"></div>
<div class="p-menu__header">Sort By</div>
<button class="p-menu__item" role="menuitem" type="button">
<span>Name</span>
<i class="p-menu__chevron ph-bold ph-caret-right" aria-hidden="true"></i>
</button>
</div>
| Part | Value |
|---|---|
| Surface | 238px minimum, 34px radius, 10px 16px padding |
| Background | rgba(246,246,246,0.73) over blur(30px) saturate(120%) |
| Edge | 0.5px dark hairline outside the shape, a bright 1px rim and a glow band along the top and bottom, no rim on the sides |
| Shadow | --p-shadow-overlay: 0 8px 48px rgba(0,0,0,0.25) |
| Row | 40px minimum, 8px vertical padding, so a wrapped label makes a 60px row |
| Label | 17px, weight 400, line height 22px, tracking -0.43px, #1A1A1A |
| Label start | 22px from the surface edge, 58px after a glyph |
| Glyph | 17px, in a 28px column on the leading side, 8px before the label |
| Pressed | Full-height 190 × 40 capsule, rgba(0,0,0,0.07) |
| Separator | 1px rgba(0,0,0,0.10), 10px above and below. 206px wide, or 190px in a menu with glyphs |
| Shortcut | 15px, weight 510, #727272. Disabled rgba(0,0,0,0.15) |
| Chevron | 15px bold, in the label colour |
| Section title | 13px, weight 510, tracking -0.08px, rgba(0,0,0,0.26), lined up with the capsule |
| Disabled | rgba(0,0,0,0.26) |
| Destructive | #FF383C |
The glyph leads the label and the shortcut or chevron trails it. Put .p-menu__icon anywhere in the row: it always sorts to the leading side. A menu where some rows have glyphs and some don’t reads best when the rows without one sit in their own section, like the Sort By rows above.
The pressed capsule fills the row’s height but not its width. It stops 8px short of each side, so separators run wider than the capsule. The separator narrows to the capsule’s width once the menu has a glyph column, which keeps the rules lined up with the content.
Disabled ink, section titles, separators, and the pressed capsule are translucent black rather than flat greys. They darken whatever the glass shows, so they stay visible over any backdrop instead of dissolving into a grey that happens to match.
A long label wraps onto a second line rather than truncating. The row grows to fit.
The slots are the pointer menu’s: .p-menu__item, .p-menu__separator, .p-menu__header, .p-menu__icon, .p-menu__shortcut, .p-menu__chevron, .p-menu__check. Adding .p-menu-touch restyles them.
Checkmarks
.p-menu__check adds a 30px column in front of the glyph for a 17px, weight 590 checkmark. Keep the slot on unchecked rows too, empty, so every label starts at the same place. Mark the rows role="menuitemradio" or role="menuitemcheckbox" with aria-checked, since the glyph alone says nothing to a screen reader.
Palette
<div class="p-menu p-menu-touch" role="menu">
<div class="p-menu-touch__palette" role="group" aria-label="Quick actions">
<button class="p-menu-touch__tile" role="menuitem" type="button">
<i class="ph-bold ph-copy" aria-hidden="true"></i><span>Copy</span>
</button>
…
</div>
<div class="p-menu__separator" role="separator"></div>
…
</div>
A palette puts the two or three most frequent commands in a row of tiles above the list. Each tile is 56px tall with a 20px radius, a 15px glyph over a 12px, weight 510 label. Tiles share the width equally, 10px in from the surface edge and 5px apart, so three fit a 238px menu at 69px each.
A pressed, focused, or aria-pressed="true" tile takes the row capsule’s tint. Consider the palette for commands people reach for every time (Copy, Share, Delete) and the list for everything else.
Compact size
.p-menu-touch--compact is the menu raised from text, like Edit or Find. Same 40px pitch at 15px, tracking -0.43px, with 10px row padding. Separators get 8.5px above and below, 3px less per block than the default. Labels start 18px from the edge (54px after a glyph), and section titles drop to 12px at 16px.
A subtitle goes in .p-menu-touch__label with a .p-menu-touch__subtitle at 13px. The row grows to fit it: 58px in the compact menu.
Spacious size
.p-menu-touch--spacious keeps the earlier touch menu: 15px text at +0.2px tracking on a 50px row, the capsule inset 5px inside it, and the glyph on the trailing side. Use it when a layout was built around the taller rows. New menus should use the default.
Edit menu bar
<div class="p-menu-touch-bar" role="menu" aria-label="Edit">
<button class="p-menu-touch-bar__item" role="menuitem" type="button">Cut</button>
<button class="p-menu-touch-bar__item" role="menuitem" type="button">Copy</button>
<button class="p-menu-touch-bar__item" role="menuitem" type="button">Paste</button>
<button class="p-menu-touch-bar__more" type="button" aria-label="More">
<i class="ph-bold ph-caret-right" aria-hidden="true"></i>
</button>
</div>
The horizontal menu that appears over selected text. A 44px capsule of the menu’s glass, 15px commands at -0.6px tracking, 20px in from the leading edge, divided by 1 × 18px rules. The trailing 36px circle pages to more commands. Keep it to three or four commands, and put the rest behind the circle.
Context menus
A context menu is the same surface, raised by a long press, next to a preview of the item.
<div class="p-menu-touch-context">
<div class="p-menu-touch-preview">
<!-- a picture of the thing being acted on -->
</div>
<div class="p-menu p-menu-touch" role="menu">
...
</div>
</div>
.p-menu-touch-context lays out a preview and a 250px menu 16px apart. The menu sits below the preview, lined up with its leading edge. Set data-p-menu-place to change that:
| Value | Placement |
|---|---|
(none) or below-start | Below, leading edges aligned |
below-end | Below, trailing edges aligned |
trailing | Beside, after the preview, top edges aligned |
leading | Beside, before the preview, top edges aligned |
.p-menu-touch-preview gives the preview its 30px radius. Add .p-menu-touch--context to a menu used on its own to get the 250px width.
context_menus.js handles the press. Point an element at a hidden menu and it opens on a long press, on right-click, and on Shift+F10 or the context-menu key:
<div data-p-context-menu="#file-menu">…</div>
<div class="p-menu p-menu-touch p-menu-touch--context" id="file-menu" role="menu" hidden>…</div>
<script type="module" src="/js/context_menus.js"></script>
It waits 500ms and cancels if the pointer moves more than 10px, so a scroll never opens a menu. The menu floats against the element that raised it with the same geometry: 16px below, leading edge first, then trailing. Without room below it moves beside the element, then above it, and it is always clamped 8px inside the viewport. For a large surface like a canvas, where the element is too big to anchor to, add data-p-context-menu-at="pointer" to open at the press point instead.
It closes on Escape, an outside tap, or a choice. Focus moves to the first item and returns to where it was.
A long press has no visible affordance, so people find it by already knowing it is there. Offer every command in the context menu somewhere else too: a toolbar, a swipe action, or a detail screen.
Popovers
Filters
Narrow the list to what you are looking for.
<div class="p-popover p-popover-touch" data-p-popover-place="bottom">
<h3>Filters</h3>
<p>Narrow the list to what you are looking for.</p>
</div>
| Part | Value |
|---|---|
| Radius | 38px |
| Arrow | 56 x 13px |
| Padding | 20px, or none when the popover holds a list |
| Width | 240px minimum, 360px maximum |
| Background | rgba(255,255,255,0.84) over blur(65px) contrast(0.595) brightness(1.682) |
| Arrow position | Centre kept at least 72px from either end of the edge it sits on |
| Shadow | 0 10px 100px rgba(0,0,0,0.30), no rim |
The touch popover is a bigger, softer object than the pointer one: 38px radius against 12px, and a 56 x 13 arrow against 35.44 x 10.5. The arrow is the same silhouette scaled to the new box, so the tip stays rounded and the shoulders keep their easing. Its centre never comes closer than 72px to a corner, so the arrow never runs into the curve.
The material is near-white rather than the menu’s glass. The filter flattens and lifts the backdrop before the white goes on top, so the popover reads as #EEEEEE over a mid-grey page and pure white over a white one. It carries a wide, soft shadow and no edge rim.
A popover holding a .p-list-touch drops its padding and clips, so the rows run to the popover’s own edges.
Placement, floating, and the .active toggle are shared with the pointer popover, so popovers.js drives both without changes. Set data-p-popover-place to top, bottom, left, or right; the arrow moves to the opposite edge.
<script type="module" src="/js/popovers.js"></script>
Dark mode
| Part | Light | Dark |
|---|---|---|
| Menu background | rgba(246,246,246,0.73), blur(30px) saturate(120%) | rgba(32,32,32,0.80), blur(30px) brightness(80%) saturate(500%) |
| Edge hairline | rgba(0,0,0,0.18), sides 0.22 | rgba(0,0,0,0.60), sides 0.50 |
| Top and bottom rim | white at 0.85 and 0.60 | white at 0.20 and 0.12 |
| Label | #1A1A1A | #F5F5F5 |
| Pressed capsule | rgba(0,0,0,0.07) | rgba(255,255,255,0.08) |
| Separator | rgba(0,0,0,0.10) | rgba(255,255,255,0.11) |
| Disabled and section titles | rgba(0,0,0,0.26) | rgba(255,255,255,0.28) |
| Shortcut | #727272 | rgba(255,255,255,0.60) |
| Destructive | #FF383C | #FF4245 |
| Shadow | 0 8px 48px rgba(0,0,0,0.25) | 0 8px 48px rgba(0,0,0,0.45) |
| Popover background | rgba(255,255,255,0.84) | rgba(0,0,0,0.60) over blur(65px) |
In dark mode the pressed capsule, separators, and dimmed ink turn to translucent white. They sit lighter than the glass, not darker, which is how the system draws them: a darker line on a dark menu disappears.
The dark menu keeps the colour of what is behind it and only drops its brightness. The system does that with a luminosity blend, which CSS cannot apply to a backdrop. Puppertino darkens and saturates the blurred backdrop under a charcoal tint instead, so a blue wallpaper still reads as blue through the menu. It is close, though a little greener than the system over saturated blues.
Accessibility
Give the menu role="menu" and each item role="menuitem", and make the items real buttons or links. Mark separators role="separator".
Disabled items use aria-disabled="true" rather than the disabled attribute, so they stay reachable and announce their state instead of disappearing from the tab order.
Focus lights the row’s capsule and draws the 3.5px accent focus band around it, so the keyboard position is visible even where a finger would leave no trace. Palette tiles and the edit bar’s commands use the same band. With a pointer on iPad, hovering a row shows the capsule too.
Rows are 40px, under the 44px touch minimum, because they sit edge to edge in a column: the whole row is the target and the next target starts where it ends. Avoid putting anything smaller than a row, like a separate button inside a row, in a menu.
A menu raised by a long press needs a keyboard path too. The control that opens it should open it on Enter or Space as well.
Reduced transparency and browsers without backdrop-filter swap the material for an opaque surface on the menu, the edit bar, and the popover. Increased contrast replaces the rims with a 1px keyline and draws separators in the label colour.
The pointer versions
menus.css and popovers.css each ship both platforms. .p-menu and .p-popover alone are the macOS components: smaller type, tighter rows, a full-bleed highlight, and a 12px popover radius. Adding .p-menu-touch or .p-popover-touch swaps them to iOS metrics.