CSS & Color Strategy
Ramp-Based Color System
zudotext uses a ramp-based color system implemented in the @takazudo/color-themes package (packages/color-themes/). Colors flow through two tiers: a small set of ramps (Tier 1) and a map of semantic tokens that each reference a ramp stop (Tier 2).
Tier 1: Ramps
Each scheme defines three ramps of OKLCH (or hex) color stops:
base— 5 stops, lightest → darkest neutralaccent— 3 stops, the accent huestate— one color each fordanger,success,warning,info
These map directly to CSS custom properties:
--palette-base-0 through --palette-base-4
--palette-accent-0 through --palette-accent-2
--palette-state-danger, --palette-state-success, --palette-state-warning, --palette-state-infoapplyRamps() writes these on :root.
Tier 2: Semantic Colors
Semantic colors are resolved by walking each semantic token's RampRef — a { base: n } / { accent: n } / { state: role } reference, or a literal color string — through the ramps, and provide meaningful names for UI elements:
--theme-bg-primary /* Main background */
--theme-bg-secondary /* Slightly darker background */
--theme-bg-surface /* Surface/card background */
--theme-text-primary /* Main text color */
--theme-text-secondary /* Muted text */
--theme-accent /* Accent color (from palette yellow) */
--theme-accent-subtle /* Accent with low opacity */
--theme-border /* Border color */
--theme-danger /* Error/danger color (from palette red) */
--theme-danger-strong /* Strong danger (from palette bright red) */
--theme-on-accent /* Text on accent backgrounds */
--theme-hover-overlay /* Subtle hover effect */
--theme-hover-bg /* Standard hover background */
--theme-hover-fg /* Text color on hover */
--theme-selection /* Selection highlight */
--theme-cursor /* Cursor color */
/* Editor Markdown highlights — applied by CodeMirror's HighlightStyle */
--theme-editor-heading /* # heading text (default ramp ref: { state: "info" }) */
--theme-editor-strong /* **bold** text (default ramp ref: { accent: 1 }) */
--theme-editor-emphasis /* *italic* text (default ramp ref: { accent: 0 }) */
--theme-editor-link /* [link](url) and URLs (default ramp ref: { state: "info" }) */
--theme-editor-quote /* > blockquote text (default ramp ref: { base: 3 }) */
--theme-editor-inline-code /* `inline code` text (default ramp ref: { state: "success" }) */resolveScheme() generates these by walking each semantic token's RampRef through the scheme's ramps, mixing in derived values (hover/active overlays, accentSubtle, notify chip tints) via deriveSemanticRampDefaults(). Since epic #5893 a scheme owns both a light and a dark ModeMap, so callers slice one out with resolveMode(structure, mode) first:
function resolveScheme(resolved: ResolvedMode): ColorSettings {
const sem = resolveSemanticColors(resolved); // walks map.semantic through ramps
return {
bgPrimary: resolveRampRef(resolved.map.bg, resolved.ramps),
bgSecondary: sem.bgSecondary,
bgSurface: sem.surface,
accent: sem.accent, // resolves { accent: 1 } -> ramps.accent[1]
accentSubtle: sem.accentSubtle, // colorMixAlpha(accent, 0.1, bg)
danger: sem.danger, // resolves { state: "danger" } -> ramps.state.danger
// ...
};
}Hover Token Convention
Interactive elements use three dedicated hover tokens for consistent hover feedback across the entire UI:
| CSS Variable | Tailwind Utility | Purpose |
|---|---|---|
--theme-hover-overlay | bg-overlay | Subtle overlay effect (rarely used directly) |
--theme-hover-bg | bg-hover | Standard hover background for interactive items |
--theme-hover-fg | text-hover-fg | Text color on hover (ensures contrast on hover-bg) |
Standard pattern
For interactive list items, menu items, dropdown options, sidebar entries, and similar:
<button class="hover:bg-hover hover:text-hover-fg transition-colors">
Menu item
</button>For container-level hovers (cards, rows) where child text colors should remain unchanged:
<div class="hover:bg-hover transition-colors">
<span class="text-fg">Title</span>
<span class="text-fg-muted">Subtitle</span>
</div>When NOT to use hover tokens
Danger actions: Use
hover:bg-hover hover:text-dangerorhover:bg-danger/10to preserve the danger color signalAccent/primary buttons: Keep
hover:opacity-85orhover:bg-accent— these have their own color logicSelected/active states: Use
bg-accent-subtlefor keyboard-highlighted or selected items (not a hover effect)Opacity-based hovers: Toolbar icon buttons that use
hover:opacity-100are fine as-is
Built-in Schemes
Exactly one scheme is bundled — "default" — and it carries both a light and a dark ModeMap (epic #5893 D2; see resolveMode() above). Pre-epic there were two separate schemes, default-dark and default-light; they were merged into this single scheme's two modes, since they already shared one set of ramps and differed only in their map.
A scheme switch (today, the no-op case of re-selecting "Default") re-seeds the entire persisted {ramps, map} structure from the selected preset (seedColorStructure()) — there is no per-scheme partial override list to maintain. Which of the active scheme's two modes is actually displayed is a separate concern, controlled by general.colorMode ("system" | "light" | "dark", @takazudo/app-defaults) rather than by the scheme itself — see Workspace Config and the manual's Settings and Themes for that setting and the effective-mode store that resolves "system" against the OS.
Scheme Application
Schemes are applied at startup and when the user changes the color scheme, the color mode, or tweaks a ramp, in settings:
// Apply ramp stops as CSS custom properties (--palette-base-*, --palette-accent-*, --palette-state-*)
applyRamps(ramps);
// Apply resolved semantic colors as CSS custom properties (--theme-*)
applyColors(colorSettings);applyTheme(themeName) is a convenience wrapper that looks up a built-in scheme by name and calls applyRamps() for callers (e.g. app boot) that only need the Tier 1 ramp variables set immediately; the Colors tab itself calls applyRamps() and applyColors() directly on every edit so ramp tweaks are reflected live.
Both functions set properties on document.documentElement.style, making them available globally via CSS var().
Tailwind CSS 4 Integration
The frontend uses Tailwind CSS 4 for utility-first styling. Tailwind's configuration references the CSS custom properties set by the color theme system, allowing utility classes to adapt to the active theme:
/* Tailwind uses theme colors via CSS variables */
.bg-primary { background-color: var(--theme-bg-primary); }
.text-primary { color: var(--theme-text-primary); }Color in Settings
The color field in .zudotext.settings.json is a ColorStructure — { ramps, map }, exactly the shape produced by seedColorStructure(). Editing a ramp stop or a semantic token's RampRef in the Colors tab mutates this structure directly and persists it; there is no separate "override" layer sitting on top of a base theme. When the user selects a new color scheme, color is replaced wholesale with seedColorStructure(getSchemeByName(name)) — every ramp and mapping resets to that scheme's defaults.