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. |
Dark mode
Dark mode is toggled by adding .dark to :root (<html>). The theme toggle in the header does this at click time and persists to localStorage under the key shelldocs-theme.
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).
// 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.
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 CLI flag
shelldocs init --theme <shadcn|fuma|nextra> records a theme preset in the scaffold. Currently informational : all presets ship the same tokens.css. Future work: fuma and nextra presets will ship as additional stylesheet drops.
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.
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.