# Theming > Pick a colour preset, override tokens, ship a brand logo, swap the layout variant, register a custom stylesheet. How to make a ShellDocs site look like yours. Source: https://shelldocs.shellui.dev/docs/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. For a tinted palette without writing any CSS, pick a colour preset. ## Colour presets ```csharp builder.Services.AddShellDocs(o => { o.Theme = ShellDocsTheme.Harbor; }); ``` | Preset | Colour | |---|---| | `Graphite` | Neutral greys. The default, and what this site uses. | | `Harbor` | Blue | | `Lagoon` | Teal | | `Forest` | Green | | `Savanna` | Amber on sand | | `Ember` | Red-orange | | `Jacaranda` | Purple | Pick one below to see it. Switch the site between light and dark and the example follows. **Colour presets** [Live preview](https://shelldocs.shellui.dev/demos/presets.html) A preset tints the backgrounds, borders and muted text with its hue, and uses its colour for the active sidebar item, focus rings, link underlines, the primary button and other accents. Each one has a light and a dark palette, and every text colour keeps a contrast of at least 4.5:1. The callout colours (`--info`, `--warning`, `--error`, `--success`) stay the same in every preset. `shelldocs init --theme harbor` writes the `o.Theme` line for a new site. Without `--theme` the site gets Graphite, with the line as a commented-out example. ### How a preset is applied The presets are CSS in `tokens.css`, one light and one dark block each, keyed on a `data-shelldocs-theme` attribute: ```css :root:has(:where([data-shelldocs-theme="harbor"])) { --primary: oklch(0.520 0.170 255); /* ⦠*/ } :root.dark:has(:where([data-shelldocs-theme="harbor"])) { /* ⦠*/ } ``` `o.Theme` makes `DocsLayout` and `HomeLayout` render that attribute, so a preset works on a static export too. The `:where()` keeps the rules as specific as plain `:root` and `:root.dark`, so [your own token overrides](#overriding-tokens) in a later stylesheet still win: pick the preset closest to your brand and override the few tokens that differ. A page that uses `ShellDocs.Tokens` without the layouts can put the attribute on any element. > **Shadcn, Fuma and Nextra** > > Before `0.1.18-alpha`, `ShellDocsTheme` had the values `Shadcn`, `Fuma` and `Nextra`, and none of them changed anything. They are now obsolete aliases of `Graphite`: code that sets one still compiles, with a warning, and gets the default palette. `shelldocs init --theme` takes the preset names. ## The token file `ShellDocs.Tokens` ships one CSS file : `_content/ShellDocs.Tokens/tokens.css`. It declares two `:root` blocks : the light-mode defaults (Graphite) and the `:root.dark` overrides that kick in when the theme toggle is set to dark. After them come the [colour presets](#colour-presets). > **Where the tokens come from** > > 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`: ```html ``` Then override any token in `brand.css`: ```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 | Name | 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: ```css :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. ```csharp // 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"); ``` > **Prefer inline SVG** > > 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: | Name | 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. | ```csharp 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: ```css @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. ## 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: ```html