Menus and popovers
tier 2 — classes
One dropdown panel style, for a short list of actions opened from a control, and one
anchored panel for content. The header's user menu is the same
.menu class — there is not a second panel style for the frame.
role="menu". That role promises arrow-key
navigation and a roving tabindex. A panel that claims it without implementing it is worse for
a screen-reader user than one that claims nothing, because the items stop being reachable the
way they appear to be. These are ordinary links and buttons that tab. Put
aria-expanded on the trigger and leave the panel unlabelled by role.
Anchored under a trigger
.menu-anchor wraps the trigger and the panel. It is what makes the panel's
position: absolute resolve against the trigger rather than the page, so nothing is
measured. Open it — the demo is live.
data-menu-toggle on the trigger hands it to drSimpleUi.menu:
opening and closing, one panel open at a time, a click outside or on an item closing it, and
Escape closing it and returning focus to the trigger. The closed state is the
hidden attribute rather than a class, which is what takes the panel out of the
tab order and the accessibility tree as well as out of the layout. An app holding the state
in C# leaves the attribute off and renders hidden itself.
The panel hangs off the anchor's trailing edge and is 220px at its narrowest, so a
trigger narrower than that overhangs on the leading side. That is right at the trailing end of a
row — a table row's actions, a toolbar's overflow. For a trigger near the leading edge, add
.menu--start to the panel and it aligns to the anchor's leading edge instead.
A menu is for actions. A list of values to pick one of is a
<select> or a segmented control; a list of
places is the sidebar.
<div style="padding-bottom:210px">
<div class="menu-anchor">
<button class="btn" type="button" data-menu-toggle aria-expanded="false">
<i class="ri-more-2-fill"></i> Actions
</button>
<div class="menu menu--start" hidden>
<span class="menu-label">This order</span>
<button class="menu-item" type="button">
<i class="ri-user-shared-line"></i> Reassign<span class="menu-item-note">R</span>
</button>
<button class="menu-item" type="button">
<i class="ri-time-line"></i> Snooze<span class="menu-item-note">S</span>
</button>
<button class="menu-item" type="button" aria-disabled="true">
<i class="ri-git-merge-line"></i> Merge
</button>
<hr class="menu-sep" />
<a class="menu-item" href="#"><i class="ri-external-link-line"></i> Open in the warehouse</a>
<button class="menu-item menu-item--danger" type="button">
<i class="ri-delete-bin-line"></i> Discard reservation
</button>
</div>
</div>
</div>
Four shapes
A definition, a key/value detail block, a small table, and one with an action in it. The line
to watch: a popover holds content. One trailing action that acts on what it is
explaining is fine; a list of actions is a .menu.
popovertargetaction="hide" closes it from inside with no script at all.
Stock reserved for an order but not yet picked. A hold expires after 48 hours and the stock returns to the pool.
| Warehouse | Free | Held |
|---|---|---|
| Rotterdam | 128 | 12 |
| Hamburg | 0 | 4 |
| Lyon | 46 | 0 |
Lines 3 and 5 have no free stock in any warehouse. The order ships when either arrives.
<div class="dr-row-wrap dr-gap-2">
<button class="btn" type="button" popovertarget="pop-define">What is a hold?</button>
<div class="popover" id="pop-define" popover>
<strong class="popover-title">Hold</strong>
<p>Stock reserved for an order but not yet picked. A hold expires after 48 hours and the
stock returns to the pool.</p>
</div>
<button class="btn" type="button" popovertarget="pop-kv">ORD-4182 detail</button>
<div class="popover" id="pop-kv" popover>
<strong class="popover-title">ORD-4182</strong>
<div class="kv"><span class="k">Placed</span><span class="v">3 Aug, 09:14</span></div>
<div class="kv"><span class="k">Customer</span><span class="v">Priya Nair</span></div>
<div class="kv"><span class="k">Region</span><span class="v">EU-West</span></div>
<div class="kv"><span class="k">Lines</span><span class="v">5, two on hold</span></div>
</div>
<button class="btn" type="button" popovertarget="pop-table">Stock by warehouse</button>
<div class="popover" id="pop-table" popover style="max-width:320px">
<strong class="popover-title">On hand</strong>
<table class="table">
<thead><tr><th>Warehouse</th><th class="col-num">Free</th><th class="col-num">Held</th></tr></thead>
<tbody>
<tr><td>Rotterdam</td><td class="col-num">128</td><td class="col-num">12</td></tr>
<tr><td>Hamburg</td><td class="col-num">0</td><td class="col-num">4</td></tr>
<tr><td>Lyon</td><td class="col-num">46</td><td class="col-num">0</td></tr>
</tbody>
</table>
</div>
<button class="btn" type="button" popovertarget="pop-action">Two lines on hold</button>
<div class="popover" id="pop-action" popover>
<strong class="popover-title">Awaiting stock</strong>
<p>Lines 3 and 5 have no free stock in any warehouse. The order ships when either arrives.</p>
<div class="dr-row dr-end dr-gap-1" style="margin-top:10px">
<button class="btn btn-sm" type="button" popovertarget="pop-action" popovertargetaction="hide">Close</button>
<button class="btn btn-sm btn-primary" type="button">Split the order</button>
</div>
</div>
</div>
Dismissing it
There is no JavaScript behind this. Put .menu-scrim immediately
before the panel: a transparent full-viewport element that catches the
click which closes the menu. It needs no z-index of its own — coming first in the markup
is what puts the panel above it. Handle Escape on the wrapper as well, or the
menu can only be closed with a pointer.
The demo above omits it: a fixed element covering the viewport would swallow every click on this page.
<div class="menu-anchor" @onkeydown="HandleKeyDown">
<button class="btn" type="button" aria-expanded="@_open" @onclick="Toggle">Actions</button>
@if (_open)
{
<div class="menu-scrim" @onclick="Close"></div>
<div class="menu">
<button class="menu-item" type="button">Reassign</button>
</div>
}
</div>
Popover
.popover is the other panel: a small amount of content anchored to the
control that opened it — why a row is pending, what a number means. A menu's items are
links and buttons; if what you are showing is prose, it belongs here. Click a button above.
The popover attribute is required, and so is opening it from a
popovertarget control. The platform then supplies the top layer, light dismiss,
Escape and focus handling, so unlike .menu this needs no scrim and
no key handler. There is no drSimpleUi.popover and there does not need to be.
popovertarget also makes the button the popover's implicit
anchor, so nothing here declares an anchor-name. Declaring one means a
unique --dashed-ident per instance, which in practice is an inline style on every
popover in the app. position-try-fallbacks: flip-block
moves the panel above the trigger when there is no room below — scroll this page until one
is near the bottom and open it.
Priority 2 carries a four-hour response target measured from the first warehouse update, not from when the order reached this queue.
Reassigning does not restart the clock. Only a priority change does.
Below 0.60 the reservation is never auto-applied. Between 0.60 and 0.90 it waits for a human. Above 0.90 it applies if the action type is on the automation list.
<div class="dr-row-wrap">
<button class="btn" type="button" popovertarget="sla-detail">Why is this overdue?</button>
<div class="popover" id="sla-detail" popover>
<strong class="popover-title">Response target breached 2h ago</strong>
<p>
Priority 2 carries a four-hour response target measured from the first warehouse
update, not from when the order reached this queue.
</p>
<p>Reassigning does not restart the clock. Only a priority change does.</p>
</div>
<button class="btn btn-secondary" type="button" popovertarget="confidence-detail">
Confidence 0.87
</button>
<div class="popover" id="confidence-detail" popover>
<strong class="popover-title">How the score is read</strong>
<p>
Below 0.60 the reservation is never auto-applied. Between 0.60 and 0.90 it waits for a
human. Above 0.90 it applies if the action type is on the automation list.
</p>
</div>
</div>