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

Getting started

this site, not the package

Install 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. */
Redefine only tokens the library already declares Never declare a -- 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.

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 &amp; 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.