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)

The script

tier 2 — classes

drSimpleUi holds generic UI behaviour only: hover hints, theme settings, clipboard, toasts, confirmations, and delegated wiring for menus and tabs. App-specific interop stays in the app's own script.

The CSS never depends on the script. Every class on these pages applies with scripting blocked. What the script adds is behaviour — and the two frame pieces that need state, the collapsed rail's flyout and the user menu, are CSS and C# respectively for exactly that reason.
From C#, inject IDrSimpleUi. Register it once with builder.Services.AddDrSimpleUi(). Every member is a JavaScript call, so none of them can run during prerendering — call them from an event handler, or from OnAfterRenderAsync(firstRender: true). The wrappers do not swallow that exception: a call that silently did nothing would be far harder to find than one that threw.

Toasts from C#

ToastAsync takes the message and a ToastKind — the enum maps to the script's own family names, which are also the CSS modifier suffixes, so ToastKind.Go and .toast-go cannot drift apart.

There is no C# equivalent of the remover function toast() returns: it is a JavaScript function and cannot cross the boundary. Dismissing a toast early stays a JavaScript concern.

@inject IDrSimpleUi Ui

<div style="display:flex; flex-wrap:wrap; gap:8px">
    <button class="btn btn-go" type="button" @onclick="Approved">Success toast</button>
    <button class="btn btn-warn" type="button" @onclick="Lowered">Warning toast</button>
    <button class="btn btn-danger" type="button" @onclick="Failed">Failure, stays until dismissed</button>
</div>

@code {
    private Task Approved() => Ui.ToastAsync(
        "Dispatched ORD-4209", ToastKind.Go, title: "Sent to the warehouse");

    private Task Lowered() => Ui.ToastAsync("Automation lowered to level 1", ToastKind.Warn);

    // timeout 0 stays until dismissed — right for a failure the reader has to act on.
    private Task Failed() => Ui.ToastAsync(
        "Could not reach the warehouse. Nothing was written.", ToastKind.Danger, timeoutMs: 0);
}

Confirmation from C#

await Ui.ConfirmAsync(…) returns a bool, so the decision reads as ordinary C# control flow. It is built on <dialog>.showModal() rather than window.confirm(), which in Blazor Server blocks the circuit for as long as it is open.

nothing yet
@inject IDrSimpleUi Ui

<div style="display:flex; flex-wrap:wrap; gap:8px; align-items:center">
    <button class="btn" type="button" @onclick="Apply">Confirm an action</button>
    <button class="btn btn-danger" type="button" @onclick="Discard">Confirm something destructive</button>
    <span class="badge">@_outcome</span>
</div>

@code {
    private string _outcome = "nothing yet";

    private async Task Apply()
    {
        var ok = await Ui.ConfirmAsync(
            "Apply the reservation?",
            "The order will be rerouted to EU-West.",
            confirmLabel: "Apply");

        _outcome = ok ? "applied" : "cancelled";
    }

    // danger reddens the confirm button and focuses Cancel, so a stray Enter does
    // not delete anything.
    private async Task Discard()
    {
        var ok = await Ui.ConfirmAsync(
            "Discard the reservation?",
            "This cannot be undone.",
            confirmLabel: "Discard",
            danger: true);

        _outcome = ok ? "discarded" : "kept";
    }
}

Loading it

Three files, and the order matters. boot.js goes in <head> so the stored theme is applied before first paint — loaded at the end of the body it would produce a dark flash on a light-theme app.

<head>
    <link rel="stylesheet" href="_content/DR.Simple_UI/lib/remixicon/remixicon.css" />
    <link rel="stylesheet" href="_content/DR.Simple_UI/css/DR.Simple_UI.css" />
    <script src="_content/DR.Simple_UI/js/DR.Simple_UI.boot.js"></script>
</head>
<body>
    <!-- … -->
    <script src="_content/DR.Simple_UI/js/DR.Simple_UI.js"></script>
</body>

Toasts

drSimpleUi.toast(message, opts) creates the stack on first use, so the app renders nothing and positions nothing. It returns a function that removes that toast early.

timeout: 0 means it stays until dismissed — right for a failure the reader has to act on. The message is inserted as text, never as markup: it usually carries a value from the server, and this is the one place an app hands the library one.

<div style="display:flex; flex-wrap:wrap; gap:8px">
    <button class="btn btn-go" type="button"
            onclick="drSimpleUi.toast('Dispatched ORD-4209', { kind: 'go', title: 'Sent to the warehouse' })">
        Success toast
    </button>
    <button class="btn btn-warn" type="button"
            onclick="drSimpleUi.toast('Automation lowered to level 1', { kind: 'warn' })">
        Warning toast
    </button>
    <button class="btn btn-danger" type="button"
            onclick="drSimpleUi.toast('Could not reach the warehouse. Nothing was written.', { kind: 'danger', timeout: 0 })">
        Failure, stays until dismissed
    </button>
</div>

Confirmation

await drSimpleUi.confirm({ … }) resolves true or false. It is built on <dialog>.showModal(), which is the point: the platform supplies the top layer, a focus trap, Escape-to-close and inert content behind, none of which a div-based overlay gets without a lot of code that is usually subtly wrong.

Use it instead of window.confirm(), which cannot be styled, blocks the thread, and in Blazor Server blocks the circuit for as long as it is open. With danger: true the confirm button turns red and focus starts on Cancel, so a stray Enter does not delete anything.

<div style="display:flex; flex-wrap:wrap; gap:8px">
    <button class="btn" type="button"
            onclick="drSimpleUi.confirm({ title: 'Apply the reservation?', message: 'The order will be rerouted to EU-West.', confirm: 'Apply' }).then(ok => drSimpleUi.toast(ok ? 'Applied' : 'Cancelled', { kind: ok ? 'go' : 'info' }))">
        Confirm an action
    </button>
    <button class="btn btn-danger" type="button"
            onclick="drSimpleUi.confirm({ title: 'Discard the reservation?', message: 'This cannot be undone.', confirm: 'Discard', danger: true }).then(ok => drSimpleUi.toast(ok ? 'Discarded' : 'Kept', { kind: ok ? 'danger' : 'info' }))">
        Confirm something destructive
    </button>
</div>

Copy without a handler

data-copy="…" copies a literal; data-copy-target="#sel" copies that element's text; an empty data-copy-target inside a .code-block copies its <pre>. The click is delegated from document, so a button rendered by a later Blazor render works with nothing wired — and nothing is re-bound on every render, which is how per-element handlers leak.

np_live_8f2c1d
<div style="display:flex; flex-wrap:wrap; gap:8px; align-items:center">
    <button class="btn" type="button" data-copy="dotnet add package DR.Simple_UI">
        <i class="ri-file-copy-line"></i><span>Copy the install command</span>
    </button>
    <code id="the-token">np_live_8f2c1d</code>
    <button class="btn btn-sm" type="button" data-copy-target="#the-token">
        <i class="ri-file-copy-line"></i><span>Copy</span>
    </button>
</div>

data-menu-toggle on the trigger inside a .menu-anchor handles opening, one panel at a time, the outside click, Escape, and returning focus to the trigger. The closed state is the hidden attribute, not a class: a hidden panel is out of the tab order and out of the accessibility tree, which an opacity: 0 one is not.

.menu-anchor hugs its trigger and the panel hangs off the anchor's inline-end edge. An app that wants a full-width anchor sets align-self: stretch on it.

<div style="padding-bottom:170px">
    <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>
            <button class="menu-item" type="button"><i class="ri-pencil-line"></i> Rename</button>
            <button class="menu-item" type="button"><i class="ri-file-copy-line"></i> Duplicate</button>
            <hr class="menu-sep" />
            <button class="menu-item menu-item--danger" type="button"><i class="ri-delete-bin-line"></i> Delete</button>
        </div>
    </div>
</div>

Tabs, wired for you

data-tabs on a .tabs container is mostly there for the keyboard: arrow keys, Home and End move between tabs, and only the selected tab is a tab stop, so Tab steps past the tablist instead of through every tab in it. It also shows the panel named by aria-controls and hides the rest.

Leave data-tabs off when the component drives aria-selected from C#, or two things set it.

Twelve waiting on a first response.
<div>
    <div class="tabs" role="tablist" data-tabs aria-label="Views">
        <button class="tab" role="tab" aria-selected="true" aria-controls="js-p1" type="button">Open</button>
        <button class="tab" role="tab" aria-selected="false" aria-controls="js-p2" type="button" tabindex="-1">Decided</button>
        <button class="tab" role="tab" aria-selected="false" aria-controls="js-p3" type="button" tabindex="-1">Everything</button>
    </div>
    <div class="tab-panel" role="tabpanel" id="js-p1">Twelve waiting on a first response.</div>
    <div class="tab-panel" role="tabpanel" id="js-p2" hidden>Decided in the last 24 hours.</div>
    <div class="tab-panel" role="tabpanel" id="js-p3" hidden>Everything, archived included.</div>
</div>

Hover hints

data-tip on any control renders one bubble for the whole page through event delegation, so content rendered later needs no wiring. data-tip-pos="left|right|top|bottom" pins a side. Write what the control does and its consequence — a tip that repeats the label earns nothing.

The bubble itself is a single .dr-tip appended to <body> — fixed, so a card's or a table's overflow never clips it — and .dr-tip--visible is what fades it in. You never write either class; they are listed here because they are in the stylesheet and an app may need to see them in DevTools.

Elements inside .sidebar are skipped on purpose: the collapsed rail has a CSS flyout, and both firing draws two tooltips. An app suppresses hints of its own with drSimpleUi.tips.gate; the library has no knowledge of what is suppressing them.

Awaiting stock Degraded
<div class="dr-row-wrap dr-gap-2">
    <button class="btn" type="button" data-tip="Reload the list from the server. Unsaved notes are kept.">
        <i class="ri-refresh-line"></i> Refresh
    </button>
    <button class="btn btn-icon" type="button" aria-label="Export"
            data-tip="Downloads every row that matches the current filters, as CSV.">
        <i class="ri-download-2-line"></i>
    </button>
    <button class="btn btn-danger" type="button"
            data-tip="Deletes the reservation. The order goes back to unreserved." data-tip-pos="right">
        <i class="ri-delete-bin-line"></i> Discard
    </button>
    <span class="badge badge-warn" data-tip="Two of five lines have no stock in any warehouse."
          data-tip-pos="bottom">Awaiting stock</span>
    <span class="health-badge health-badge--degraded" data-tip="One of three warehouses is not answering."
          data-tip-pos="top"><span class="health-dot"></span> Degraded</span>
</div>

Suppressing hints

Assign a predicate to drSimpleUi.tips.gate and it is asked before every hint. It stays JavaScript-only, because a predicate cannot cross the interop boundary.

// Suppress hints while something else owns the reader's attention — a tour, a modal,
// a drag in progress. The library has no knowledge of what is suppressing them.
drSimpleUi.tips.gate = el => !document.body.classList.contains('tour-active');