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.
An unhandled error has occurred.
Sedna.UI
v0.17.0 · main

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 — and 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 and .sheet from the bottom, with .sheet-handle inside the header's own top padding; .drawer-body is the only scrolling part, and data-sheet on a <dialog> opts the handle into the drag (the script carries .sheet--dragging for its duration) — a close button is still what makes a sheet dismissible.

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" data-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, and the header carries the .list-row lockup with --flush because the header already supplies the padding. Everything that scrolls goes in .drawer-body, and aria-labelledby points at the title rather than 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-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 wider drawer

Live — open it. A drawer is 420px, which is enough for a form and too little for a picture or a wide table. Set --drawer-width on the drawer to any length: 560px here, or max(420px, 33vw) for one that grows with the window. The viewport still caps it, so it is edge to edge on a phone. A .sheet takes the same knob, and its default is 480px.

racking-bay-4-after-the-move.jpg

Racking in bay 4, restocked after the move
Size3.4 MB
Taken12 Sep 2026

Photograph by CHUTTERSNAP on Unsplash.

<button class="btn" type="button" onclick="document.getElementById('photo-detail').showModal()">
    <i class="ri-image-line"></i> Open the photo
</button>

<dialog class="drawer" id="photo-detail" aria-labelledby="photo-detail-title"
        style="--drawer-width: 560px">
    <div class="drawer-header">
        <h3 id="photo-detail-title">racking-bay-4-after-the-move.jpg</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="media-frame media-frame--cover">
            <img src="https://images.unsplash.com/photo-1587293852726-70cdb56c2866?w=960&amp;h=600&amp;fit=crop&amp;q=70&amp;auto=format"
                 width="960" height="600" loading="lazy" alt="Racking in bay 4, restocked after the move" />
        </div>
        <div class="kv"><span class="k">Size</span><span class="v">3.4 MB</span></div>
        <div class="kv"><span class="k">Taken</span><span class="v">12 Sep 2026</span></div>
        <p class="form-hint sedna-mb-0">Photograph by CHUTTERSNAP on Unsplash.</p>
    </div>
    <div class="drawer-footer">
        <button class="btn" type="button" onclick="this.closest('dialog').close()">Close</button>
    </div>
</dialog>

A sheet of actions

Live — open it. A sheet whose body is a list of actions, the phone form of a menu: each row is a <button class="list-row"> filling the <li>, so the whole row is the target and the count is announced. The body is .drawer-body--flush so the rows meet the sheet's edges; reach for a sheet rather than a menu when the actions need a second line each, or when a 36px menu row is a small target on a touch screen.

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" data-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 drawer-body--flush">
        <ul class="list">
            <li>
                <button class="list-row" 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" 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" 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 as its own component

Live — open it. ISednaOverlays.ShowAsync<TComponent, TResult> renders your component, opens the <dialog> it wrote and completes with what it closed with — Overlay.CloseAsync(result), or the default for Escape and CancelAsync(). The component takes a cascading SednaOverlay and puts its Id on the dialog; every element and word on screen is still in your file, and the drawer is removed only once it has slid out. Place <SednaOverlayHost /> once in the layout. The same works for a .modal or a .sheet, and ISednaUi.ShowModalAsync(id) opens any of the three when the dialog is already in the page.

The <dialog> has to be in the component's first render, because it is opened as soon as the component has rendered once. Load data inside it, behind a .skeleton. Use a nullable result, such as bool?, where cancelled has to differ from a real false.

Owner Alex Fischer
@inject ISednaOverlays Overlays

<div class="sedna-row-wrap sedna-gap-3" style="align-items:center">
    <button class="btn" type="button" @onclick="Reassign">
        <i class="ri-user-shared-line"></i> Reassign ORD-4209
    </button>
    <span class="form-hint">Owner</span>
    <span class="badge">@_owner</span>
</div>

@code {
    private string _owner = "Alex Fischer";

    // ReassignOrder writes its own <dialog class="drawer">. This renders it, opens it,
    // and waits for the owner it closes with — null for Escape, Cancel or the close button.
    private async Task Reassign()
    {
        var owner = await Overlays.ShowAsync<ReassignOrder, string>(
            new() { [nameof(ReassignOrder.Current)] = _owner });

        if (owner is not null) _owner = owner;
    }
}

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; its closed state is visibility: hidden rather than a translate off-screen, or 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 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>