Theming
Pick a preset, swap brand colors via theme.yaml, and override individual tokens — colors, radii, shadows, and nav treatment.
A Publier theme defines the entire visual identity — colors, radii, shadows, and even the sidebar pill treatment. Each theme is a pair of CSS blocks: one for color and radius tokens, and a sibling :root[data-theme="<name>"] block for Publier’s structural tokens (shadow tint, nav-item pill, code-block corners). Swap a preset for a new look. Override the primary color in theme.yaml for your brand. Drop into raw CSS for anything beyond that.
Quick start
-
Pick a preset in
theme.yamlat the project root:theme.yaml preset: maple -
Override brand colors in the same file:
theme.yaml preset: maplecolors:primary: "oklch(65% 0.2 50)"accent: "oklch(70% 0.17 65)" -
Done. Publier picks up
theme.yamlautomatically at build time and emits the overrides as CSS variables for the active theme.
Built-in presets
| Preset | Character | Dark mode |
|---|---|---|
maple | Warm amber, rounded pills, soft shadows — the flagship “rich” theme | Espresso brown |
light | Clean white + indigo accent, minimal | — |
dark | Classic slate + indigo | — |
ocean | Deep blue-green, marine | Dark-first |
dusk | Warm purple twilight | Dark-first |
emerald | Forest green + teal accents | Forest |
purple | Violet + magenta | Plum |
ruby | Crimson red + pink | Burgundy |
solar | Gold + amber | Dark-first |
aspen | Earthy beige + sage | Warm dark |
almond | Warm off-white | — |
neutral | Pure gray-scale | Slate |
catppuccin | Pastel “mocha” palette | Dark-first |
vitepress | Minimal like VitePress | Dark |
Theme switching is driven by the data-theme attribute on <html>. ThemeToggle manages it for you.
How it works
Publier theming is driven by two layers of CSS variables:
- Color and radius tokens (
--color-*,--radius-*) — drive the semantic utilities (text-primary,bg-base-100,border-base-300,btn btn-primary, …) used across the site. - Publier structural tokens (
--publier-*) — declared on:root[data-theme="<name>"]. These cover layout geometry, shadow tinting, sidebar nav-item pill, and code-block corner radii.
Because everything flows through CSS custom properties, swapping a theme changes geometry and elevation, not just colors — rounded-box on the same component renders at 0.5rem on light and 0.75rem on maple.
theme.yaml reference
All fields are optional. Overrides compose on top of the imported preset — you can pick maple for the geometry and override only primary for your brand.
name: My Docspreset: maple # informational — the actual preset is imported in global.css
colors: primary: "oklch(65% 0.2 50)" primary-content: "oklch(100% 0 0)" accent: "oklch(70% 0.17 65)" base-100: "oklch(99% 0.015 80)" # page background base-200: "oklch(96% 0.02 80)" # elevated surface base-300: "oklch(92% 0.04 80)" # muted / borders base-content: "oklch(18% 0.06 40)" # body text
typography: heading: '"Geist", sans-serif' body: '"Inter", sans-serif' mono: '"JetBrains Mono", monospace'
layout: nav-height: "4rem" sidebar-width: "17rem" toc-width: "14rem" page-max-width: "52rem"
logo: light: "/logo-light.svg" dark: "/logo-dark.svg"favicon: "/favicon.svg"Colors must be a valid CSS color value — OKLCH is the canonical format. Hex values still work but are converted to OKLCH at build time.
Token surface
Colors — semantic tokens
Each theme exposes the same set of semantic color tokens; the matching utility classes are generated automatically.
| Token | Utility class | Role |
|---|---|---|
--color-primary | bg-primary, text-primary | Brand color — links, active states, CTAs |
--color-primary-content | text-primary-content | Text on a primary fill |
--color-accent | bg-accent, text-accent | Secondary accent |
--color-accent-content | text-accent-content | Text on an accent fill |
--color-base-100 | bg-base-100 | Page background |
--color-base-content | text-base-content | Default body text |
--color-base-200 | bg-base-200 | Elevated surfaces (cards, callouts) |
--color-base-300 | border-base-300, bg-base-300 | Borders, hairlines, muted surfaces |
--color-error | bg-error, text-error | Error / danger |
--color-success | bg-success, text-success | Success states |
--color-warning | bg-warning, text-warning | Warnings |
--color-info | bg-info, text-info | Info callouts |
Compose with Tailwind opacity modifiers: bg-primary/50, border-primary/20. Component classes like btn btn-primary and alert alert-success pick up the tokens automatically — most surfaces don’t need a utility at all.
Radii
| Token | Used by | Default (light) | maple |
|---|---|---|---|
--radius-selector | checkboxes, radios, small inputs | 0.5rem | 0.5rem |
--radius-field | inputs, selects, buttons | 0.5rem | 0.5rem |
--radius-box | cards, modals, alerts | 0.75rem | 0.75rem |
Shadows + structural layer — Publier tokens
Publier’s --publier-* tokens cover layout geometry, elevation tinting, and component-specific structural details. They live on :root[data-theme="<name>"] inside each theme file.
| Token | Purpose |
|---|---|
--publier-shadow-color | Shadow tint (e.g. maple uses warm brown so cards feel grounded in the amber palette rather than casting a neutral gray shadow) |
--publier-shadow-opacity | Shadow alpha |
--publier-radius-2xl | Extra large radius |
--publier-nav-height | Top-nav height |
--publier-sidebar-width | Sidebar width |
--publier-toc-width | Table-of-contents column width |
--publier-page-max-width | Prose max-width on docs pages |
Nav-item treatment — Publier tokens
These tokens style the sidebar links automatically — no CSS needed. A theme sets them, and every docs page picks up the new shape.
| Token | Purpose | maple value |
|---|---|---|
--publier-nav-item-radius | Link border-radius | 0.5rem (rounded pill) |
--publier-nav-item-padding-y | Vertical padding | 0.4375rem |
--publier-nav-item-padding-x | Horizontal padding | 0.75rem |
--publier-nav-item-font-weight | Default weight | 500 |
--publier-nav-item-bg-hover | Background on hover | oklch(95% 0.04 80) (amber tint) |
--publier-nav-item-bg-active | Background when aria-current="page" | oklch(65% 0.2 50) (solid amber) |
--publier-nav-item-color-active | Text color on active | oklch(100% 0 0) |
--publier-nav-item-shadow-active | Subtle elevation on active | 0 1px 2px 0 oklch(18% 0.06 40 / 0.12) |
Prose rhythm
Not exposed as a theme token on purpose. Paragraph spacing, line-heights, and heading sizes are owned by the @tailwindcss/typography plugin applied to docs prose — those values are tuned for readable long-form docs and custom overrides regress more than they help in most cases. If you need to shift a specific element, add a CSS file to customCss in publier.config.yaml and target .prose (or a more specific descendant) directly.
Code blocks — Publier tokens
Applies to every code block in your site: the astro-expressive-code frame (auto-wired by Publier) and any plain Shiki output on custom pages. The rounded/sharp choice carries across every surface.
| Token | Purpose | Default (sharp) | maple value |
|---|---|---|---|
--publier-code-block-radius | Code block frame border-radius | 0 | 0.625rem |
--publier-code-inline-radius | Inline <code> border-radius | 0 | 0.375rem |
--publier-code-inline-padding-y | Inline code vertical padding | 0 | 0.125rem |
--publier-code-inline-padding-x | Inline code horizontal padding | 0.25rem | 0.375rem |
Override via theme.yaml:
code: block-radius: "0.5rem" # rounded code blocks inline-radius: "0.25rem"Internally, --publier-code-block-radius drives expressive-code’s --ec-brdRad token so the copy button, file-name tab, and line-highlight frames all pick up the new corner.
Dark mode
Every theme defines its own block with an explicit color-scheme and an independent set of OKLCH values. Publier switches themes via the data-theme attribute on <html> — the ThemeToggle flips it for you (storing the choice in localStorage and re-applying across navigations).
@plugin "daisyui/theme" { name: "light"; color-scheme: "light"; --color-primary: oklch(47% 0.24 270); /* indigo-600 */ /* … */}
@plugin "daisyui/theme" { name: "dark"; color-scheme: "dark"; --color-primary: oklch(70% 0.22 270); /* lighter indigo for dark bg */ /* … */}Advanced: bespoke theme
Create your own theme CSS file and import it alongside the preset:
@plugin "daisyui/theme" { name: "my-brand"; default: true; color-scheme: "light";
--color-base-100: oklch(100% 0 0); --color-base-200: oklch(98% 0.005 240); --color-base-300: oklch(94% 0.01 240); --color-base-content: oklch(20% 0.03 260);
--color-primary: oklch(47% 0.24 270); --color-primary-content: oklch(100% 0 0); --color-accent: oklch(78% 0.16 210); --color-accent-content: oklch(15% 0.03 260); --color-error: oklch(52% 0.24 28); --color-success: oklch(62% 0.17 150);
--radius-selector: 0.25rem; --radius-field: 0.25rem; --radius-box: 0.375rem;}
:root[data-theme="my-brand"] { --publier-shadow-color: oklch(18% 0.04 260); --publier-shadow-opacity: 0.06; --publier-nav-item-radius: 0; --publier-nav-item-bg-active: transparent; --publier-nav-item-color-active: var(--color-primary);}@import "tailwindcss";@plugin "daisyui" { themes: false; }@import "./my-theme.css";@import "@publier/shell/tailwind/preset.css";Using utility classes
All tokens are live as utility classes in MDX and components:
<div class="bg-primary/10 border border-primary/20 rounded-box p-6 shadow-sm"> Themed card — uses the current preset's radius and shadow.</div>
<button class="btn btn-primary">Call to action</button>
<div class="alert alert-info">The alert component picks up the theme tokens automatically.</div>CSS cascade order
The CSS cascade is deterministic: Publier injects the theme preset tokens first, then anything you list under customCss in publier.config.yaml. Your customCss entries therefore take precedence over the preset, and anything you load inside an @layer block you author yourself is governed by standard CSS ordering rules.