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 Sedna.UI
Host page
The order matters: boot.js in <head> so the stored theme is
applied before first paint, your own override file after the library stylesheet, and
Sedna.UI.js before blazor.web.js so window.sednaUi
exists before an interactive component can call into it. The viewport meta also needs
viewport-fit=cover, or every
safe-area inset reports 0px on every device.
<!-- In <head>. viewport-fit=cover is required: without it env(safe-area-inset-*)
reports 0px on every device and the safe-area handling never runs. -->
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<!-- Before the asset lines, or it does not apply to them. Without it a direct hit on
a sub-route resolves every relative path below against that route and 404s. -->
<base href="/" />
<!-- Applies the stored theme before first paint: -->
<script src="_content/Sedna.UI/js/Sedna.UI.boot.js"></script>
<link rel="stylesheet" href="_content/Sedna.UI/lib/remixicon/remixicon.css" />
<link rel="stylesheet" href="_content/Sedna.UI/css/Sedna.UI.css" />
<link rel="stylesheet" href="css/brand.css" />
<!-- At the top of <body>: the state illustrations, once. -->
<SednaStateArt />
<!-- At the end of <body>: -->
<script src="_content/Sedna.UI/js/Sedna.UI.js"></script>
The status bar
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, and supply all
four rows — a state with no row renders as an empty bar. The
Status bar page has the state table.
<div id="components-reconnect-modal">
<div class="status-bar status-bar--reconnecting">
<i class="ri-wifi-off-line"></i>
<span>
Connection lost. Reconnecting…
<!-- Blazor fills these two by id, if they are there. -->
<span class="status-bar-attempt">
attempt <span id="components-reconnect-current-attempt">1</span>
of <span id="components-reconnect-max-retries">8</span>
</span>
</span>
</div>
<div class="status-bar status-bar--paused">
<i class="ri-pause-circle-line"></i>
<span>Paused. Your work is held on the server.</span>
</div>
<div class="status-bar status-bar--failed">
<i class="ri-close-circle-line"></i><span>Could not reconnect.</span>
<button class="status-bar-action" type="button" onclick="location.reload()">
<i class="ri-refresh-line"></i> Retry
</button>
</div>
<div class="status-bar status-bar--expired">
<i class="ri-error-warning-line"></i><span>This session has expired on the server.</span>
<button class="status-bar-action" type="button" onclick="location.reload()">
<i class="ri-refresh-line"></i> Reload
</button>
</div>
</div>
Registration
Only needed for the C# side — ISednaUi wraps toasts, dialogs, 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).
// Program.cs — registers ISednaUi, the typed wrapper over the browser API.
builder.Services.AddSednaUi();
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: #d62828;
--brand-hover: #b81f1f;
--brand-active: #8f1818;
--brand-soft: #ff7a70;
--brand-text: #ff9b93;
--accent: #ff7a70;
--sidebar-active: #d62828;
}
/* The light variant needs the readable-on-white variants. */
:root[data-variant="light"] {
--brand-soft: #d62828;
--brand-text: #b81f1f;
--accent: #b81f1f;
}
/* --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 Sidebar 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; matching drops the query string and the fragment, ignores a trailing slash, and
requires a prefix match to end on a path segment, so 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 — a sidebar whose parameters are unchanged is
skipped on re-render and goes on showing the previous address. Call
sednaUi.scrollPageTop from the same handler, because .page is the
only scroll container.
@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
Add the MCP server —
{ "type": "http", "url": "https://www.sedna-ui.com/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.