Shell
tier 1 — the frameThe page shells: the full three-part layout, the two reduced ones, and the rhythm inside them. This looks identical in every app and is never restyled per project.
<AppShell> and there will not be one — copy the markup below, and report a
frame problem against the library rather than overriding it locally.
Full shell
.layout is the flex row that fills the viewport — shown here at a fixed
height — .content is the column beside the sidebar, and .page is
the only scroll container. Width is two knobs on .sidebar,
--sidebar-width (220px) and --sidebar-width-collapsed (56px);
nothing else about the frame changes with them.
<div class="layout" style="height:460px; overflow:hidden">
<aside class="sidebar">
<div class="brand">
<div class="brand-logo" style="background:var(--brand)"></div>
<div class="brand-text">
<strong>Dispatch Console</strong>
<span class="brand-sub">Northwind Retail</span>
</div>
</div>
<nav class="nav">
<div class="nav-scroll">
<a class="nav-link active" href="#"><i class="ri-inbox-line"></i><span>Queue</span><span class="nav-count">3</span></a>
<a class="nav-link" href="#"><i class="ri-chat-3-line"></i><span>Orders</span></a>
<a class="nav-link" href="#"><i class="ri-check-double-line"></i><span>Decided</span></a>
<div class="nav-section">
<span class="nav-section-label">Administration</span>
<a class="nav-link" href="#"><i class="ri-robot-line"></i><span>Automation</span></a>
<a class="nav-link" href="#"><i class="ri-settings-3-line"></i><span>Settings</span></a>
</div>
<a class="nav-status-card nav-status-card--ok" href="#">
<span class="nav-status-label">Workflow API</span>
<span class="nav-status-row">
<span class="nav-status-dot"></span>
<span class="nav-status-state">Connected</span>
</span>
</a>
<a class="nav-status-card nav-status-card--fail" href="#">
<span class="nav-status-label">MySQL</span>
<span class="nav-status-row">
<span class="nav-status-dot"></span>
<span class="nav-status-state">Unreachable</span>
</span>
</a>
</div>
<div class="nav-tools">
<a class="nav-link nav-link-tool" href="#"><i class="ri-book-2-line"></i><span>Documentation</span><i class="ri-external-link-line nav-link-ext"></i></a>
</div>
</nav>
</aside>
<div class="content">
<header class="topbar">
<button class="topbar-btn topbar-btn--start" aria-label="Collapse sidebar" data-tip="Collapse the sidebar to an icon rail."><i class="ri-side-bar-line"></i></button>
<div class="topbar-spacer"></div>
<div class="topbar-section">
<span class="topbar-version">v4.2.1</span>
<span class="health-badge health-badge--live"><span class="health-dot"></span> Live</span>
</div>
<button class="topbar-btn" aria-label="Help" data-tip="What is this console?"><i class="ri-question-line"></i></button>
<div class="user-widget">
<button class="user-trigger">
<span class="user-avatar"><i class="ri-user-line"></i></span>
<span class="user-info">
<span class="user-name">Alex Fischer</span>
<span class="user-email">alex.fischer@example.com</span>
</span>
</button>
<a class="user-signout" href="#" aria-label="Sign out" data-tip="Sign out of the console."><i class="ri-logout-box-r-line"></i></a>
</div>
</header>
<div class="page">
<a class="back-link" href="#"><i class="ri-arrow-left-line"></i> Back to the queue</a>
<h1>Approval queue</h1>
<p class="lede">Items the AI proposed and is waiting on a human decision for.</p>
<div class="card">
<div class="card-head">
<strong>ORD-4209</strong>
<span class="badge badge-warn">Pending</span>
</div>
<div class="card-body">
<p class="form-hint">Reservation: reroute to EU-West (confidence 0.87).</p>
</div>
</div>
</div>
</div>
</div>
Bare shell
.bare-layout — topbar and body, no sidebar, for the page that comes before
there is a workspace to navigate: choosing one, accepting an invitation, a first-run step.
Where there is nothing to offer at all use .auth-layout below;
Common pages has the finished pages.
<div class="bare-layout" style="height:280px">
<header class="topbar">
<div class="bare-brand">
<div class="brand-logo" style="background:var(--brand)"></div>
<span>Acme Ops</span>
</div>
</header>
<div class="page">
<div class="card" style="max-width:440px">
<div class="card-head"><strong>Choose a workspace</strong></div>
<div class="card-body sedna-col">
<p class="form-hint">Your account is a member of two.</p>
<a class="btn" href="#"><i class="ri-building-line"></i> Northwind Retail</a>
<a class="btn" href="#"><i class="ri-building-line"></i> Contoso Logistics</a>
</div>
</div>
</div>
</div>
Auth layout
Sign-in, sign-out, “check your e-mail”, an invitation — one centred card and no
navigation. .auth-card is capped rather than stretched.
Use your organisation account.
Trouble signing in? Ask the service desk.
<div class="auth-layout" style="min-height:0; padding: var(--space-9)">
<div class="auth-brand">
<span class="brand-logo" style="width:32px; height:32px; background:var(--brand)"></span>
Dispatch Console
</div>
<div class="card auth-card">
<div class="card-head"><strong>Sign in</strong></div>
<div class="card-body sedna-col">
<p class="form-hint">Use your organisation account.</p>
<button class="btn btn-primary"><i class="ri-microsoft-fill"></i> Continue with Microsoft</button>
</div>
</div>
<p class="auth-foot">Trouble signing in? Ask the service desk.</p>
</div>
Full-bleed layout
For content that owns the whole area below the header — a map, a board, a diagram editor.
Use it instead of .page and never nested inside it, and put
.full-layout-fill on the child that takes the height that is left.
<div class="layout">
<aside class="sidebar">…</aside>
<div class="content">
<header class="topbar">…</header>
<div class="full-layout">
<div class="toolbar">…</div>
<div class="full-layout-fill">The map fills whatever is left.</div>
</div>
</div>
</div>
The rhythm between page sections
Every direct child of .page after the first gets --page-gap above
it, so a card after a card needs no class and no style="margin-top". Set
--page-gap: 0 on .page to manage the rhythm yourself, or
.sedna-mt-0 on the one child that should sit flush; inside
.card-body there is no equivalent — reach for
.sedna-col with a gap step.
Reconciliation
Two cards and an alert, with nothing spacing them by hand.
Last run
Next run
<div class="page" style="padding: var(--space-7); border: 1px dashed var(--border)">
<div class="page-head">
<div class="page-head-text">
<h2>Reconciliation</h2>
<p>Two cards and an alert, with nothing spacing them by hand.</p>
</div>
</div>
<div class="alert alert-info">
<i class="ri-information-line"></i>
<span>Seven accounts differ in cost centre and are held for approval.</span>
</div>
<div class="card">
<div class="card-head"><h3>Last run</h3></div>
<div class="card-body">1 204 of 1 211 accounts matched.</div>
</div>
<div class="card">
<div class="card-head"><h3>Next run</h3></div>
<div class="card-body">Tonight at 02:00 CET.</div>
</div>
</div>
A footer at the bottom of a short page
.page--fill makes the page a flex column, so a .page-foot —
a footer, a build line — sits at the bottom of a page that is shorter than the
viewport rather than under its last paragraph, and a lone
.empty-state--page centres itself between the top
and the foot. Opt in per page: a flex column stops sibling margins collapsing, which the
block page relies on. This site's own pages use it.
Exports
Nothing has been exported this month.
<div class="page page--fill" style="height:300px; border:1px dashed var(--border-strong); border-radius:var(--radius-surface)">
<div class="page-head">
<div>
<h1>Exports</h1>
<p class="lede">Nothing has been exported this month.</p>
</div>
</div>
<div class="card">
<div class="card-body">Two lines of content, and then the page ends.</div>
</div>
<footer class="page-foot text-muted text-sm">
Northwind Retail · build 8841 · <a href="#">Status</a>
</footer>
</div>
Narrow screens
Add .layout--responsive to .layout and the sidebar becomes the
rail below 900px, where it is forced — so hide a collapse toggle at that width or accept
the no-op. A topbar outside a .layout opts in with
.topbar--responsive on the .topbar instead.
<div class="layout layout--responsive">
<aside class="sidebar">…</aside>
<div class="content">…</div>
</div>
On a phone, the document scrolls
.page is the frame's only scroll container, and that is the one thing a
phone's browser cannot see: Safari collapses its toolbar only while the document
scrolls, so a page scrolling inside a box keeps the full toolbar on screen for ever.
.layout--flow hands the scrolling to the document below 560px — the
shell grows with its content, the topbar sticks under the notch, a sidebar sticks to the
top of its column and a bottom bar to the bottom of the viewport — and changes
nothing above it. This site uses it. Keep the box for a page that must never move under
its header, a full-bleed map or an editor.
<div class="layout layout--flow">
<aside class="sidebar">…</aside>
<div class="content">
<header class="topbar">…</header>
<div class="page">…</div>
</div>
</div>
Skip link
The first focusable thing on the page, jumping past the navigation to the content —
off-screen rather than hidden, so it stays focusable. tabindex="-1" on
<main> is not optional: without it the jump only scrolls and focus stays
back in the navigation. Neither is Sedna.UI.js: under the host page's
<base href="/"> a bare #main means the start page, and the
script is what keeps the jump here. This site has one — press Tab on any page.
<body>
<a class="skip-link" href="#main">Skip to content</a>
<div class="layout">
<aside class="sidebar">…</aside>
<div class="content">
<header class="topbar">…</header>
<div class="page">
<main id="main" tabindex="-1">…</main>
</div>
</div>
</div>
</body>