zudo-text

検索したい単語を入力

いつでも検索バーを開ける

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 neutral

  • accent — 3 stops, the accent hue

  • state — one color each for danger, 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-info

applyRamps() 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 VariableTailwind UtilityPurpose
--theme-hover-overlaybg-overlaySubtle overlay effect (rarely used directly)
--theme-hover-bgbg-hoverStandard hover background for interactive items
--theme-hover-fgtext-hover-fgText 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-danger or hover:bg-danger/10 to preserve the danger color signal

  • Accent/primary buttons: Keep hover:opacity-85 or hover:bg-accent — these have their own color logic

  • Selected/active states: Use bg-accent-subtle for keyboard-highlighted or selected items (not a hover effect)

  • Opacity-based hovers: Toolbar icon buttons that use hover:opacity-100 are 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.