Getting started
this site, not the packageInstall the package, add five lines to the host page, and redefine seven tokens. Nothing else is required.
Install
Add the package
Pin the version. Do not use a floating version range.
dotnet add package DR.Simple_UI
Host page
The order matters. boot.js goes in <head> so the stored theme
is applied before first paint; your own override file comes after the library stylesheet, and
DR.Simple_UI.js comes before blazor.web.js so
window.drSimpleUi exists before an interactive component can call into it.
This is the block this site runs, and a test compares the two — so what you copy is what is deployed.
<!-- In <head> — applies the stored theme before first paint: -->
<script src="_content/DR.Simple_UI/js/DR.Simple_UI.boot.js"></script>
<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" />
<link rel="stylesheet" href="css/brand.css" />
<!-- At the end of <body>: -->
<script src="_content/DR.Simple_UI/js/DR.Simple_UI.js"></script>
The reconnect banner
Blazor Server injects its own reconnect UI unless the host page supplies one. Add this inside
<body>, before the component that carries the render mode. Supply all four
rows — a state with no row renders as an empty bar. The
Shell and nav page has the state table.
<div id="components-reconnect-modal">
<div class="reconnect-banner reconnect-attempting">
<i class="ri-wifi-off-line"></i>
<span>
Connection lost. Reconnecting…
<!-- Blazor fills these two by id, if they are there. -->
<span class="reconnect-attempt">
attempt <span id="components-reconnect-current-attempt">1</span>
of <span id="components-reconnect-max-retries">8</span>
</span>
</span>
</div>
<div class="reconnect-banner reconnect-paused">
<i class="ri-pause-circle-line"></i>
<span>Paused. Your work is held on the server.</span>
</div>
<div class="reconnect-banner reconnect-failed">
<i class="ri-close-circle-line"></i><span>Could not reconnect.</span>
<button class="btn btn-sm btn-danger" onclick="location.reload()">
<i class="ri-refresh-line"></i> Retry
</button>
</div>
<div class="reconnect-banner reconnect-rejected">
<i class="ri-error-warning-line"></i><span>This session has expired on the server.</span>
<button class="btn btn-sm btn-danger" onclick="location.reload()">
<i class="ri-refresh-line"></i> Reload
</button>
</div>
</div>
Registration
Only needed for the C# side — IDrSimpleUi wraps toasts, confirmations, the
clipboard, the palette, the search index and the settings the theme toggles write. The CSS
needs no registration at all.
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). They deliberately do not swallow the
exception prerendering raises: a call that silently did nothing would be far harder to find.
// Program.cs — registers IDrSimpleUi, the typed wrapper over the browser API.
builder.Services.AddDrSimpleUi();
Branding
Your whole css/brand.css, loaded after the library. Nothing else changes.
Brand tokens
--brand-tint, --brand-ring, --brand-ring-soft,
--brand-ring-check and --brand-glow are mixed from
--brand and follow it in both themes. Set them only to change the alpha the
library chose.
:root {
--brand: #e41f16;
--brand-hover: #c8170f;
--brand-active: #a81209;
--brand-soft: #ff6f66;
--brand-text: #ff8f88;
--accent: #ff6f66;
--sidebar-active: #e41f16;
}
/* The light theme needs the readable-on-white variants. */
:root[data-theme="light"] {
--brand-soft: #e41f16;
--brand-text: #c8170f;
--accent: #c8170f;
}
/* --brand-tint and the four --brand-ring/-glow tokens are mixed from
--brand and follow it on their own, in both themes. */
-- name it does not define — a later version may claim that name with a
different meaning and your app breaks on upgrade. If a value is missing,
request
it (opens in a new tab) and use an app-prefixed variable until it ships. The full list is on the
Tokens page.
The frame
The shell, sidebar, header and user widget are CSS classes like everything else. There is no
<AppShell> and there will not be one — copy the markup from
Shell and nav.
Which link is the current page
The one thing markup cannot express. Nav.CssClass(href) appends
active and Nav.AriaCurrent(href) returns "page" or
null, which Blazor omits. The class colours the item; aria-current is what is
announced.
Matching drops the query string and the fragment, ignores a trailing slash, and requires a
prefix match to end on a path segment — so /queue does not light up on
/queue-archive. The link to the root needs
NavLinkMatch.All, or it is active everywhere.
Subscribe to LocationChanged in the component that renders the
links, not in the layout around it. When a parent re-renders, Blazor only hands new
parameters to a child whose parameters differ, so a sidebar whose parameters are unchanged is
skipped and goes on rendering the previous address. Call
drSimpleUi.scrollPageTop from the same handler:
.page is the only scroll container, so navigation otherwise leaves the new page
at the old one's offset.
@inject NavigationManager Nav
<nav class="nav" style="max-width:260px">
<div class="nav-scroll">
<div class="nav-section">
<span class="nav-section-label">Frame</span>
<a class="@Nav.CssClass("/frame")" aria-current="@Nav.AriaCurrent("/frame")" href="/frame">
<i class="ri-side-bar-line"></i><span>Shell & nav</span>
</a>
<a class="@Nav.CssClass("/layouts")" aria-current="@Nav.AriaCurrent("/layouts")" href="/layouts">
<i class="ri-layout-3-line"></i><span>Layouts</span>
</a>
@* The root link needs NavLinkMatch.All. With the default Prefix it is
active on every page — the same trap the framework's NavLink has. *@
<a class="@Nav.CssClass("", match: NavLinkMatch.All)"
aria-current="@Nav.AriaCurrent("", NavLinkMatch.All)" href="">
<i class="ri-home-4-line"></i><span>Overview</span>
</a>
</div>
</div>
</nav>
For an AI agent
{ "type": "http", "url": "https://simpleui.dennisrahmen.dev/mcp" } — then copy
docs/CLAUDE.consuming-app.md (opens in a new tab) into your app's own
CLAUDE.md. The server answers questions; the rules file is what stops an agent
inventing a token name or wrapping a table in a component.