Mobile components

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.

<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>
PartValue
Surface238px minimum, 34px radius, 10px 16px padding
Backgroundrgba(246,246,246,0.73) over blur(30px) saturate(120%)
Edge0.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)
Row40px minimum, 8px vertical padding, so a wrapped label makes a 60px row
Label17px, weight 400, line height 22px, tracking -0.43px, #1A1A1A
Label start22px from the surface edge, 58px after a glyph
Glyph17px, in a 28px column on the leading side, 8px before the label
PressedFull-height 190 × 40 capsule, rgba(0,0,0,0.07)
Separator1px rgba(0,0,0,0.10), 10px above and below. 206px wide, or 190px in a menu with glyphs
Shortcut15px, weight 510, #727272. Disabled rgba(0,0,0,0.15)
Chevron15px bold, in the label colour
Section title13px, weight 510, tracking -0.08px, rgba(0,0,0,0.26), lined up with the capsule
Disabledrgba(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:

ValuePlacement
(none) or below-startBelow, leading edges aligned
below-endBelow, trailing edges aligned
trailingBeside, after the preview, top edges aligned
leadingBeside, 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.

Long press me

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>
PartValue
Radius38px
Arrow56 x 13px
Padding20px, or none when the popover holds a list
Width240px minimum, 360px maximum
Backgroundrgba(255,255,255,0.84) over blur(65px) contrast(0.595) brightness(1.682)
Arrow positionCentre kept at least 72px from either end of the edge it sits on
Shadow0 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

PartLightDark
Menu backgroundrgba(246,246,246,0.73), blur(30px) saturate(120%)rgba(32,32,32,0.80), blur(30px) brightness(80%) saturate(500%)
Edge hairlinergba(0,0,0,0.18), sides 0.22rgba(0,0,0,0.60), sides 0.50
Top and bottom rimwhite at 0.85 and 0.60white at 0.20 and 0.12
Label#1A1A1A#F5F5F5
Pressed capsulergba(0,0,0,0.07)rgba(255,255,255,0.08)
Separatorrgba(0,0,0,0.10)rgba(255,255,255,0.11)
Disabled and section titlesrgba(0,0,0,0.26)rgba(255,255,255,0.28)
Shortcut#727272rgba(255,255,255,0.60)
Destructive#FF383C#FF4245
Shadow0 8px 48px rgba(0,0,0,0.25)0 8px 48px rgba(0,0,0,0.45)
Popover backgroundrgba(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.

Copied