Connection lost. Reconnecting… attempt 1 of 8
Paused. Your work is held on the server.
Could not reconnect.
This session has expired on the server.
DR.Simple_UI
Showing main. This can be ahead of the version your app has installed — check what a class says it needs before copying it. Releases (opens in a new tab)

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.

Deliberately not 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.

Hold

Stock reserved for an order but not yet picked. A hold expires after 48 hours and the stock returns to the pool.

ORD-4182
Placed3 Aug, 09:14
CustomerPriya Nair
RegionEU-West
Lines5, two on hold
On hand
WarehouseFreeHeld
Rotterdam12812
Hamburg04
Lyon460
Awaiting stock

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.

Response target breached 2h ago

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.

How the score is read

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>