Color
zudo-doc's three-tier color strategy, palette system, color schemes, and customization.
zudo-doc uses a three-tier color strategy to keep every color on the site themeable. The default Tailwind theme is not imported, and the @theme block resets the color namespace with --color-*: initial before defining project tokens — only project tokens work. This ensures switching a color scheme updates the entire site at once.
Three-Tier Color Strategy
Colors are organized into three tiers. Each tier only references the tier above it:
| Tier | Name | Purpose | Defined In |
|---|---|---|---|
| 1 | Palette | Raw color values from the active color scheme | ColorSchemeProvider → :root |
| 2 | Semantic | Design meaning — what each color represents | src/styles/global.css @theme |
| 3 | Component | Scoped overrides for specific components | .zd-content in global.css |
This layering means you can:
- Swap the palette (change color scheme) → entire site updates
- Remap a semantic token (e.g. make
accentblue instead of cyan) → every component usingaccentupdates - Override a component token without affecting other components
Tier 1: The 16-Color Palette
zudo-doc’s color system uses a 16-color palette. Each color scheme provides values for 16 indexed color slots plus background, foreground, and selection colors.
Palette Index Convention
Every color scheme must follow this standard index mapping. This ensures components and semantic tokens work consistently across all themes:
| Index | Role | Description |
|---|---|---|
| p0 | Dark surface | Deepest surface (code blocks, overlays) |
| p1 | Danger | Red family — errors, destructive actions |
| p2 | Success | Green family — confirmations, tips |
| p3 | Warning | Yellow/amber — caution messages |
| p4 | Info | Blue family — informational highlights |
| p5 | Accent | Primary interactive color (links, CTA) |
| p6 | Neutral | Slate/cyan — borders, secondary elements |
| p7 | Secondary | Muted accent or secondary neutral |
| p8 | Muted | Gray — borders, secondary text, comments |
| p9 | Background | Page background |
| p10 | Surface | Elevated surface (panels, sidebars) |
| p11 | Text primary | Main body text |
| p12 | Accent variant | Brighter or alternate accent |
| p13 | Decorative | Purple/lavender — non-semantic decoration |
| p14 | Accent hover | Hover state for interactive elements |
| p15 | Text secondary | Secondary text or muted foreground |
📝 Note
The actual hex values differ between light and dark schemes, but the role of each index stays the same. For example, p1 is always a danger/red color — #dd3131 in light mode, #da6871 in dark mode. This consistency is what makes it safe to use palette tokens directly and easy to create new schemes.
How Palette Colors Are Injected
The ColorSchemeProvider component (src/components/color-scheme-provider.astro) reads the active color scheme and injects CSS custom properties on :root at build time:
:root {
--zd-bg: #282a36;
--zd-fg: #f8f8f2;
--zd-cursor: #f8f8f2;
--zd-sel-bg: #44475a;
--zd-sel-fg: #ffffff;
--zd-0: #21222c; /* palette slot 0 (Black) */
--zd-1: #ff5555; /* palette slot 1 (Red) */
/* ... through --zd-15 */
}
These --zd-* properties are the source of truth. Everything downstream — semantic tokens, component tokens, Tailwind utilities — resolves back to them.
Tier 2: Semantic Tokens
In src/styles/global.css, the @theme block maps palette properties into Tailwind-compatible tokens with design meaning:
@theme {
--color-*: initial; /* reset ALL Tailwind defaults */
/* Base */
--color-bg: var(--zd-bg);
--color-fg: var(--zd-fg);
--color-sel-bg: var(--zd-sel-bg);
--color-sel-fg: var(--zd-sel-fg);
/* Raw palette access (p0–p15) */
--color-p0: var(--zd-0);
--color-p1: var(--zd-1);
/* ... through --color-p15 */
/* Semantic aliases */
--color-surface: var(--zd-surface);
--color-muted: var(--zd-muted);
--color-accent: var(--zd-accent);
--color-accent-hover: var(--zd-accent-hover);
--color-code-bg: var(--zd-code-bg);
--color-code-fg: var(--zd-code-fg);
--color-success: var(--zd-success);
--color-danger: var(--zd-danger);
--color-warning: var(--zd-warning);
--color-info: var(--zd-info);
}
Once registered in @theme, these become standard Tailwind utility classes: bg-surface, text-accent, border-muted, etc.
Semantic Token Reference
| Token | Default Palette Slot | Usage |
|---|---|---|
bg | --zd-bg (p9) | Page background |
fg | --zd-fg (p11) | Primary text |
surface | p0 (Dark surface) | Panel/sidebar surfaces |
muted | p8 (Muted) | Muted text, borders, comments |
accent | p5 (Accent) | Links, active states, CTA |
accent-hover | p14 (Accent hover) | Hover state for accent |
sel-bg | --zd-sel-bg | Selection background |
sel-fg | --zd-sel-fg | Selection foreground |
code-bg | p10 (Surface) | Code block background |
code-fg | p11 (Text primary) | Inline code text |
success | p2 (Success) | Success states, confirmations |
danger | p1 (Danger) | Errors, destructive actions |
warning | p3 (Warning) | Warning messages |
info | p4 (Info) | Informational highlights |
Per-Scheme Semantic Overrides
Each color scheme can override the default palette-slot mapping via the semantic property in src/config/color-schemes.ts. Values can be either a palette index (number) or a direct color string:
"My Custom Scheme": {
// ...palette, background, foreground...
semantic: {
accent: 6, // use palette[6] — no color duplication
accentHover: 14, // use palette[14]
surface: "#f4efdd", // direct color string (not in palette)
},
},
Using palette indices instead of repeating color strings eliminates duplication and ensures semantic colors stay in sync when palette colors change (e.g., via the Color Tweak Panel).
The same number | string type (called ColorRef) is also supported for cursor, selectionBg, and selectionFg:
"My Theme": {
background: "#1a1a2e",
foreground: "#e0e0e0",
cursor: 6, // use palette[6] instead of duplicating the color
selectionBg: "#3a3a5e",
selectionFg: 15, // use palette[15]
palette: [...],
shikiTheme: "one-dark-pro",
},
The resolution logic lives in src/config/color-scheme-utils.ts — each property falls back to its default palette slot when not explicitly set.
Tier 3: Component Tokens
Some components define their own color variables that consume Tier 2 semantic tokens. These are internal implementation details.
Content Typography
The .zd-content class provides direct element styling using semantic tokens — no external typography plugin:
.zd-content {
color: var(--color-fg);
font-size: var(--text-body);
line-height: var(--leading-relaxed);
}
.zd-content :where(a) {
color: var(--color-accent);
}
.zd-content :where(code:not(pre code)) {
color: var(--color-code-fg);
background-color: var(--color-code-bg);
}
.zd-content :where(li::marker) {
color: var(--color-muted);
}
/* ... */
ℹ️ Info
Tier 3 tokens are internal to their components. When building your own UI, use Tier 2 semantic tokens or Tier 1 palette tokens directly.
Using Color Tokens
Prefer semantic tokens
Use semantic tokens for standard UI patterns:
<!-- Text -->
<p class="text-fg">Primary text</p>
<p class="text-muted">Secondary text</p>
<a class="text-accent hover:text-accent-hover">Link</a>
<!-- Backgrounds -->
<div class="bg-bg">Page background</div>
<div class="bg-surface">Panel or sidebar</div>
<!-- Borders -->
<div class="border border-muted">Bordered element</div>
Fall back to palette tokens when needed
Use p0–p15 when no semantic token fits — for badges, decorative elements, or status indicators:
<span class="text-p1">Error text (red)</span>
<span class="text-p2">Success text (green)</span>
<span class="text-p3">Warning text (yellow)</span>
<span class="text-p4">Info text (blue)</span>
Color Schemes
To change the active color scheme, edit src/config/settings.ts:
export const settings = {
colorScheme: "Default Dark", // Change this
siteName: "zudo-doc",
// ...
};
Default Themes
zudo-doc includes a light and dark default theme. See src/config/color-schemes.ts for available schemes.
Adding a Custom Color Scheme
Add a new entry to the colorSchemes object in src/config/color-schemes.ts:
"My Theme": {
background: 9,
foreground: 11,
cursor: 6,
selectionBg: 10,
selectionFg: 11,
palette: [
"#16213e", "#e74c3c", "#2ecc71", "#f39c12", // p0-3: dark surface, danger, success, warning
"#3498db", "#9b59b6", "#1abc9c", "#ecf0f1", // p4-7: info, accent, neutral, secondary
"#2c3e50", "#1a1a2e", "#2a2a4e", "#e0e0e0", // p8-11: muted, background, surface, text
"#5dade2", "#bb8fce", "#48c9b0", "#c0c0c0", // p12-15: accent variant, decorative, hover, text secondary
],
shikiTheme: "one-dark-pro",
// Optional: override semantic defaults
semantic: {
accent: 12, // use p12 as accent instead of default p5
surface: "#1f1f3a", // direct color string also works
},
},
The ColorScheme interface requires:
background,foreground—ColorRef(palette index or color string, typically p9 and p11)cursor,selectionBg,selectionFg—ColorRef(palette index or color string)palette— 16-element tuple (color slots 0–15)shikiTheme— a Shiki theme name for syntax highlightingsemantic(optional) — override default palette-slot mappings usingColorRefvalues (palette index or color string)
What NOT to Do
🚨 Color Anti-Patterns
Don’t use Tailwind defaults — they are reset to initial:
<!-- WRONG -->
<div class="bg-gray-800 text-blue-500">No visible color</div>
<!-- RIGHT -->
<div class="bg-surface text-accent">Works correctly</div>Don’t hardcode hex values — it breaks theming:
<!-- WRONG -->
<div class="bg-[#1e1e2e]">Breaks on theme switch</div>
<!-- RIGHT -->
<div class="bg-p0">Adapts to any theme</div>Don’t reference component-scoped variables in your own components:
/* WRONG — don't use hardcoded colors */
.my-component {
color: #3b82f6;
}
/* RIGHT — use semantic tokens */
.my-component {
color: var(--color-accent);
}