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)

Drawers and sheets

tier 2 — classes

A panel that comes in from an edge, for a secondary flow that should not lose the page behind it — a filter panel, a record's detail, a form on a phone.

Drawer or modal? A modal demands an answer before anything else happens; a drawer is a place to work that can be left. If dismissing it loses nothing, it is a drawer. The drawer sits below the modal backdrop in the z-order, so a modal opened from inside a drawer still covers it.

From each edge

Live — open one. Each is a <dialog class="drawer"> opened with showModal(), so it slides in from its edge, Escape closes it, focus stays inside it, and ::backdrop dims the page with no .drawer-scrim in the markup at all.

.drawer--start comes from the other edge. .sheet comes from the bottom — the right shape on a phone, where a side panel has nowhere to come from. .sheet-handle is decoration, not a control: dragging is a gesture an app implements, and the close button is what makes the sheet dismissible for everybody else.

.drawer-body is the only scrolling part, so the header and footer stay put. The slide survives display: none through transition-behavior: allow-discrete and @starting-style; without them the panel appears rather than slides, which is abrupt rather than broken.

Filter the queue

Filters apply as you change them.

The same panel from the inline start edge, for navigation on a narrow screen.

Reassign ORD-4209

From the bottom edge — the right shape on a phone, where a side panel has nowhere to come from.

<button class="btn" type="button" onclick="document.getElementById('filters').showModal()">
    <i class="ri-filter-3-line"></i> Filters
</button>
<button class="btn" type="button" onclick="document.getElementById('nav-drawer').showModal()">
    <i class="ri-side-bar-line"></i> From the start edge
</button>
<button class="btn" type="button" onclick="document.getElementById('assign-sheet').showModal()">
    <i class="ri-layout-bottom-line"></i> As a sheet
</button>

<dialog class="drawer" id="filters" aria-labelledby="filters-title">
    <div class="drawer-header">
        <h3 id="filters-title">Filter the queue</h3>
        <button class="modal-close" type="button" aria-label="Close"
                onclick="this.closest('dialog').close()"><i class="ri-close-line"></i></button>
    </div>
    <div class="drawer-body">
        <div class="form-field">
            <label class="form-label" for="d-group">Assignment group</label>
            <select class="form-input form-select" id="d-group">
                <option>Any</option><option selected>EU-West</option>
            </select>
        </div>
        <div class="form-field">
            <label class="form-label" for="d-priority">Priority</label>
            <select class="form-input form-select" id="d-priority">
                <option>Any</option><option selected>P2 and above</option>
            </select>
        </div>
        <p class="form-hint">Filters apply as you change them.</p>
    </div>
    <div class="drawer-footer">
        <button class="btn" type="button" onclick="this.closest('dialog').close()">Reset</button>
        <button class="btn btn-primary" type="button" onclick="this.closest('dialog').close()">Done</button>
    </div>
</dialog>

<dialog class="drawer drawer--start" id="nav-drawer" aria-labelledby="nav-drawer-title">
    <div class="drawer-header">
        <h3 id="nav-drawer-title">Queues</h3>
        <button class="modal-close" type="button" aria-label="Close"
                onclick="this.closest('dialog').close()"><i class="ri-close-line"></i></button>
    </div>
    <div class="drawer-body">
        <p>The same panel from the inline start edge, for navigation on a narrow screen.</p>
    </div>
</dialog>

<dialog class="drawer sheet" id="assign-sheet" aria-labelledby="assign-sheet-title">
    <div class="sheet-handle"></div>
    <div class="drawer-header">
        <h3 id="assign-sheet-title">Reassign ORD-4209</h3>
        <button class="modal-close" type="button" aria-label="Close"
                onclick="this.closest('dialog').close()"><i class="ri-close-line"></i></button>
    </div>
    <div class="drawer-body">
        <p>From the bottom edge — the right shape on a phone, where a side panel has nowhere to come from.</p>
    </div>
    <div class="drawer-footer">
        <button class="btn btn-primary" type="button" onclick="this.closest('dialog').close()">Reassign</button>
    </div>
</dialog>

A record's detail

Live — open it. The commonest use: the row stays on screen behind the panel, so the reader keeps their place in the list. The header carries the .list-row lockup with --flush, because the header already supplies the padding.

Everything that scrolls goes in .drawer-body. Point aria-labelledby at the title, not at the panel: the title is what a screen reader announces when focus enters.

ORD-4209 Northwind Retail · EU-West
StatePending
Lines5
Reserved3 of 5
Raised31 Jul 2026, 09:14

  1. 09:14 Order received from the storefront.
  2. 09:15 Three of five lines reserved automatically.
  3. 09:18 Held for a decision — two lines are out of stock.
<button class="btn" type="button" onclick="document.getElementById('order-detail').showModal()">
    <i class="ri-side-bar-right-line"></i> Open ORD-4209
</button>

<dialog class="drawer" id="order-detail" aria-labelledby="order-detail-title">
    <div class="drawer-header">
        <div class="list-row list-row--flush">
            <span class="avatar"><i class="ri-shopping-bag-3-line"></i></span>
            <span class="list-main">
                <span class="list-title" id="order-detail-title">ORD-4209</span>
                <span class="list-sub">Northwind Retail &middot; EU-West</span>
            </span>
        </div>
        <button class="modal-close" type="button" aria-label="Close"
                onclick="this.closest('dialog').close()"><i class="ri-close-line"></i></button>
    </div>
    <div class="drawer-body">
        <div class="kv"><span class="k">State</span><span class="v"><span class="badge badge-warn">Pending</span></span></div>
        <div class="kv"><span class="k">Lines</span><span class="v">5</span></div>
        <div class="kv"><span class="k">Reserved</span><span class="v">3 of 5</span></div>
        <div class="kv"><span class="k">Raised</span><span class="v">31 Jul 2026, 09:14</span></div>
        <hr class="divider" />
        <ol class="timeline">
            <li>
                <span class="timeline-when">09:14</span>
                <span class="timeline-what">Order received from the storefront.</span>
            </li>
            <li>
                <span class="timeline-when">09:15</span>
                <span class="timeline-what">Three of five lines reserved automatically.</span>
            </li>
            <li>
                <span class="timeline-when">09:18</span>
                <span class="timeline-what">Held for a decision &mdash; two lines are out of stock.</span>
            </li>
        </ol>
    </div>
    <div class="drawer-footer">
        <button class="btn" type="button" onclick="this.closest('dialog').close()">Close</button>
        <button class="btn btn-go" type="button" onclick="this.closest('dialog').close()">
            <i class="ri-check-line"></i> Dispatch what is reserved
        </button>
    </div>
</dialog>

A sheet of actions

Live — open it. A sheet whose body is a list of actions, which is the phone form of a menu. Each row is a real <button> filling the <li>, so the whole row is the target and the count is announced.

Use a sheet rather than a menu when the actions need a second line each, or when the reader is on a touch screen where a 36px menu row is a small target.

ORD-4209

<button class="btn" type="button" onclick="document.getElementById('order-actions').showModal()">
    <i class="ri-more-2-line"></i> Order actions
</button>

<dialog class="drawer sheet" id="order-actions" aria-labelledby="order-actions-title">
    <div class="sheet-handle"></div>
    <div class="drawer-header">
        <h3 id="order-actions-title">ORD-4209</h3>
        <button class="modal-close" type="button" aria-label="Close"
                onclick="this.closest('dialog').close()"><i class="ri-close-line"></i></button>
    </div>
    <div class="drawer-body">
        <ul class="list">
            <li>
                <button class="list-row dr-w-full text-start" type="button"
                        onclick="this.closest('dialog').close()">
                    <i class="ri-send-plane-line"></i>
                    <span class="list-main">
                        <span class="list-title">Dispatch what is reserved</span>
                        <span class="list-sub">Three of five lines</span>
                    </span>
                </button>
            </li>
            <li>
                <button class="list-row dr-w-full text-start" type="button"
                        onclick="this.closest('dialog').close()">
                    <i class="ri-user-shared-line"></i>
                    <span class="list-main">
                        <span class="list-title">Reassign</span>
                        <span class="list-sub">Currently Alex Fischer</span>
                    </span>
                </button>
            </li>
            <li>
                <button class="list-row dr-w-full text-start" type="button"
                        onclick="this.closest('dialog').close()">
                    <i class="ri-timer-line"></i>
                    <span class="list-main">
                        <span class="list-title">Hold for stock</span>
                        <span class="list-sub">Re-check every hour</span>
                    </span>
                </button>
            </li>
        </ul>
    </div>
</dialog>

A drawer that is not modal

.drawer-scrim and .drawer-scrim--open are for a drawer the reader can deliberately leave open while working on the page behind it. Closed state is visibility: hidden, not just a translate off-screen: a panel that is merely moved away is still tabbable, and Tab walks the reader into a panel they cannot see.

A drawer with a scrim is modal in behaviour, so make it a <dialog> instead — the scrim becomes ::backdrop, and the dismissal, the focus trap and Escape stop being yours to write.

<!-- Only for a drawer that is deliberately NOT modal — one the reader can leave
     open while working on the page behind it. `.drawer-scrim` immediately after the
     panel is what dims the page; the two open classes are yours to toggle, and so
     are Escape and the focus handling. A drawer WITH a scrim is modal in behaviour,
     so it should be a <dialog> instead. -->
<div class="drawer drawer--open" role="dialog" aria-labelledby="filters-title">
    <div class="drawer-header">
        <h3 id="filters-title">Filter the queue</h3>
        <button class="modal-close" type="button" aria-label="Close"><i class="ri-close-line"></i></button>
    </div>
    <div class="drawer-body">…</div>
</div>
<div class="drawer-scrim drawer-scrim--open"></div>