✨ Publier v1 is live — a polished docs platform built for the open web.
Skip to content

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

    1. Pick a preset in theme.yaml at the project root:

      theme.yaml
      preset: maple
    2. Override brand colors in the same file:

      theme.yaml
      preset: maple
      colors:
      primary: "oklch(65% 0.2 50)"
      accent: "oklch(70% 0.17 65)"
    3. Done. Publier picks up theme.yaml automatically at build time and emits the overrides as CSS variables for the active theme.

Built-in presets

PresetCharacterDark mode
mapleWarm amber, rounded pills, soft shadows — the flagship “rich” themeEspresso brown
lightClean white + indigo accent, minimal
darkClassic slate + indigo
oceanDeep blue-green, marineDark-first
duskWarm purple twilightDark-first
emeraldForest green + teal accentsForest
purpleViolet + magentaPlum
rubyCrimson red + pinkBurgundy
solarGold + amberDark-first
aspenEarthy beige + sageWarm dark
almondWarm off-white
neutralPure gray-scaleSlate
catppuccinPastel “mocha” paletteDark-first
vitepressMinimal like VitePressDark

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:

  1. 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.
  2. 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 colorsrounded-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.

theme.yaml
name: My Docs
preset: 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.

TokenUtility classRole
--color-primarybg-primary, text-primaryBrand color — links, active states, CTAs
--color-primary-contenttext-primary-contentText on a primary fill
--color-accentbg-accent, text-accentSecondary accent
--color-accent-contenttext-accent-contentText on an accent fill
--color-base-100bg-base-100Page background
--color-base-contenttext-base-contentDefault body text
--color-base-200bg-base-200Elevated surfaces (cards, callouts)
--color-base-300border-base-300, bg-base-300Borders, hairlines, muted surfaces
--color-errorbg-error, text-errorError / danger
--color-successbg-success, text-successSuccess states
--color-warningbg-warning, text-warningWarnings
--color-infobg-info, text-infoInfo 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

TokenUsed byDefault (light)maple
--radius-selectorcheckboxes, radios, small inputs0.5rem0.5rem
--radius-fieldinputs, selects, buttons0.5rem0.5rem
--radius-boxcards, modals, alerts0.75rem0.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.

TokenPurpose
--publier-shadow-colorShadow tint (e.g. maple uses warm brown so cards feel grounded in the amber palette rather than casting a neutral gray shadow)
--publier-shadow-opacityShadow alpha
--publier-radius-2xlExtra large radius
--publier-nav-heightTop-nav height
--publier-sidebar-widthSidebar width
--publier-toc-widthTable-of-contents column width
--publier-page-max-widthProse max-width on docs pages

These tokens style the sidebar links automatically — no CSS needed. A theme sets them, and every docs page picks up the new shape.

TokenPurposemaple value
--publier-nav-item-radiusLink border-radius0.5rem (rounded pill)
--publier-nav-item-padding-yVertical padding0.4375rem
--publier-nav-item-padding-xHorizontal padding0.75rem
--publier-nav-item-font-weightDefault weight500
--publier-nav-item-bg-hoverBackground on hoveroklch(95% 0.04 80) (amber tint)
--publier-nav-item-bg-activeBackground when aria-current="page"oklch(65% 0.2 50) (solid amber)
--publier-nav-item-color-activeText color on activeoklch(100% 0 0)
--publier-nav-item-shadow-activeSubtle elevation on active0 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.

TokenPurposeDefault (sharp)maple value
--publier-code-block-radiusCode block frame border-radius00.625rem
--publier-code-inline-radiusInline <code> border-radius00.375rem
--publier-code-inline-padding-yInline code vertical padding00.125rem
--publier-code-inline-padding-xInline code horizontal padding0.25rem0.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:

src/styles/my-theme.css
@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);
}
src/styles/global.css
@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.