Theming
ShellDocs is designed to be re-skinnable via CSS variables : the shipped tokens.css sets every color, radius, spacing, and font stack, and the entire chrome cascades from there. Override the tokens, and every primitive follows.
The token file
ShellDocs.Tokens ships one CSS file : _content/ShellDocs.Tokens/tokens.css. It declares two :root blocks : the light-mode defaults and the :root.dark overrides that kick in when the theme toggle is set to dark.
Each variable value is set via hsl(...) or oklch(...) for perceptual uniformity. The shadcn-compatible naming (--background, --foreground, --primary, --muted, …) means any component library that speaks the same vocabulary drops in without extra mapping.
Overriding tokens
Add a stylesheet after tokens.css in your App.razor:
<link rel="stylesheet" href="_content/ShellDocs.Tokens/tokens.css" />
<link rel="stylesheet" href="brand.css" />
Then override any token in brand.css:
:root {
--primary: oklch(0.55 0.22 240);
--radius: 0.75rem;
--font-sans: "Söhne", ui-sans-serif, system-ui, sans-serif;
}
:root.dark {
--primary: oklch(0.75 0.18 240);
}
The cascade takes care of the rest : buttons, links, focus rings, the sidebar highlight, and every callout all pull from --primary and update immediately.
The full token surface
| Prop | Type | Default | Description |
|---|---|---|---|
--background |
color |
— | Page background. |
--foreground |
color |
— | Default text color. |
--card |
color |
— | Card / elevated surface background. |
--card-foreground |
color |
— | Text on card surfaces. |
--popover |
color |
— | Popover / floating menu background. |
--popover-foreground |
color |
— | Text inside popovers. |
--primary |
color |
— | Primary action color : buttons, links, highlights. |
--primary-foreground |
color |
— | Text on primary surfaces. |
--secondary |
color |
— | Secondary surface / tag background. |
--secondary-foreground |
color |
— | Text on secondary surfaces. |
--muted |
color |
— | Subtle background : hover states, code blocks. |
--muted-foreground |
color |
— | Secondary text, hints, timestamps. |
--accent |
color |
— | Focus rings, hover accents. Semi-transparent. |
--accent-foreground |
color |
— | Text on accent surfaces. |
--border |
color |
— | Default border color. |
--border-strong |
color |
— | Prominent border : dividers between sections. |
--input |
color |
— | Form input background. |
--ring |
color |
— | Focus-ring outline color. |
--info |
color |
— | Info callout accent. |
--warning |
color |
— | Warning callout accent. |
--error |
color |
— | Error / danger callout accent. |
--success |
color |
— | Success / tip callout accent. |
--radius |
length |
— | Base border-radius. Chrome uses calc(var(--radius) + N) for varied roundness. |
--sidebar-width |
length |
— | Sidebar column width. |
--toc-width |
length |
— | Right-rail table-of-contents width. |
--header-height |
length |
— | Top header height (TopNav variant). |
--font-sans |
stack |
— | Sans-serif stack for prose + UI. |
--font-mono |
stack |
— | Monospace stack for code + inline code. |
Dark mode
Dark mode is the dark class on the html element. The theme toggle flips it and saves the choice in localStorage under the key shelldocs-theme. The toggle is handled by shelldocs.js, so it works on a static export.
The class is the source of truth. If something else flips it, such as a component library's own theme switch, shelldocs.js notices, saves the new value, and keeps the toggle icon in step.
An inline script in App.razor reads that key on page load and applies the class before first paint : this is what prevents the light-mode flash on hard refresh. The script is added by shelldocs init automatically.
To only override dark values, scope your rules:
:root.dark {
--primary: oklch(0.75 0.18 240);
--background: hsl(220, 15%, 8%);
}
Brand logo
Set via ShellDocsOptions.LogoSvg (inline SVG, uses currentColor : recolors with the theme) or LogoLight / LogoDark (URL pair for pre-rasterized bitmaps or full-color SVGs). An inline SVG is sized to LogoHeight with its width following its viewBox, so the file needs no width or height of its own.
// Inline SVG that recolors with theme
o.LogoSvg = File.ReadAllText(Path.Combine(builder.Environment.WebRootPath, "brand/logo.svg"));
// Or theme-specific images
o.SetLogo("/img/logo-light.svg", "/img/logo-dark.svg", alt: "MyBrand");
An inline LogoSvg with stroke="currentColor" swaps between light and dark for free : one file, both themes. Reach for SetLogo only when your brand needs specific hex colors that shouldn't shift.
Before 0.1.17-alpha the rule that sizes an inline logo never matched, and an SVG without a size of its own could render 0px wide. A site that added its own .brand-logo-svg > svg rule for that can remove it.
Layout variant
ShellDocsOptions.LayoutVariant picks between the two chrome layouts:
| Prop | Type | Default | Description |
|---|---|---|---|
TopNav |
default |
— | Classic header spanning the full width, sidebar below. Best for wide catalog-style docs. |
Sidebar |
alt |
— | Floating sidebar card with brand + search + collapse baked in. No top header. Best for narrower, chapter-style docs. This site uses it. |
o.LayoutVariant = DocsLayoutVariant.Sidebar;
Both layouts share the same token surface : switching is a one-line change.
Custom fonts
--font-sans and --font-mono are the two font tokens. Load your webfont via @font-face in a stylesheet loaded after tokens.css, then override the token:
@font-face {
font-family: "MySansFont";
src: url("/fonts/my-sans.woff2") format("woff2");
font-weight: 400 900;
font-display: swap;
}
:root {
--font-sans: "MySansFont", ui-sans-serif, system-ui, sans-serif;
}
Every ShellDocs primitive picks it up automatically : no per-component override needed.
The --theme flag
shelldocs init --theme and ShellDocsOptions.Theme accept shadcn, fuma and nextra, but have no effect in this release. Every site starts from the same tokens.css. Restyle with token overrides.
Keeping page typography out
Page content is styled through the shelldocs-prose class: paragraph spacing, list padding, heading sizes, link underlines. Those rules skip any element with the not-prose class and everything inside it. Preview frames carry it already, so components in examples are not restyled. Add it to your own markup when you drop custom HTML into a page and want it left alone:
<div class="not-prose">
<!-- your own layout, untouched by page typography -->
</div>
Wide markdown tables are wrapped in a shelldocs-table box that scrolls sideways on small screens instead of widening the page.
Overriding component styles directly
Every component ships with a scoped .razor.css : Blazor CSS isolation guarantees selectors don't leak. To override, add a global stylesheet after tokens.css and target the primitive's class:
/* Slightly rounder callouts */
.callout {
border-radius: calc(var(--radius) + 4px);
}
Prefer token overrides where possible : they cascade universally and keep future ShellDocs updates from clobbering your look.
The collapsed sidebar
In the Sidebar layout the collapse button sets data-sidebar-collapsed on the page shell, and the choice is kept in localStorage. shelldocs.js does this, so it works on a static export, and the saved state is restored as soon as the script runs. Style the collapsed state through the attribute:
.docs-shell[data-sidebar-collapsed="true"] .my-wide-banner {
max-width: none;
}
The docs-sidebar-collapsed class that older versions put on the shell was removed in 0.1.14-alpha. To add a collapse button of your own, give a button inside the docs layout a data-sidebar-collapse-toggle attribute.
Interoperating with ShellUI (or any Tailwind-shaped design system)
The whole point of splitting ShellDocs.Tokens out of ShellDocs.Components : third-party design systems can adopt the same token surface. If you're layering ShellUI on top, its components read the same --primary, --background, --muted variables. One brand.css retunes both frameworks in lockstep.