color-expert

bradhannah's avatarfrom bradhannah

Expert guide for the Doggy Bag color/theme system with color theory principles

0stars🔀0forks📁View on GitHub🕐Updated Jan 9, 2026

When & Why to Use This Skill

This Claude skill serves as a comprehensive expert guide for the Doggy Bag color and theme system, bridging the gap between professional color theory and technical implementation. It empowers developers and designers to maintain visual consistency by enforcing strict CSS variable naming conventions, ensuring WCAG AA accessibility compliance, and providing structured JSON schemas for scalable theme architecture.

Use Cases

  • Design System Implementation: Guiding the creation of new dark or light theme profiles using standardized JSON structures and automated color derivation patterns.
  • Accessibility Auditing: Verifying UI contrast ratios against WCAG AA standards to ensure readability and inclusive design across different background levels.
  • Codebase Refactoring: Identifying and replacing hardcoded hex colors with theme-compliant CSS variables to support dynamic theme switching.
  • Color Strategy Development: Applying color harmony principles and the 60-30-10 rule to select effective accent and semantic colors for financial or state-based UI elements.
namecolor-expert
descriptionExpert guide for the Doggy Bag color/theme system with color theory principles
licenseMIT
compatibilityopencode
audiencedevelopers
workflowdesign

Color Expert - Theme System Guide

Use this skill when working with colors, themes, or the visual design system in Doggy Bag.

Critical Rules

NEVER Use These Patterns

  1. No hardcoded hex colors - Always use CSS variables
  2. No -dark or -light suffixes - Theme profiles handle light/dark variations
  3. No danger variable - Use error instead (consolidated naming)
  4. No inline rgba() fallbacks - Define in defaults.ts instead

Naming Convention (MUST FOLLOW)

{color}         : Primary color (text, icons, solid elements)
{color}-hover   : Hover state for buttons/interactive elements
{color}-bg      : Background fill (~0.1 opacity)
{color}-muted   : Subtle background for hover states (~0.05 opacity)
{color}-border  : Border color (~0.3 opacity)

Examples:

  • --success (primary) → --success-hover (button hover) → --success-bg (background)
  • --error (primary) → --error-hover (button hover) → --error-muted (subtle hover bg)

Anti-patterns (DO NOT USE):

  • --success-dark → Use --success-hover instead
  • --error-dark → Use --error-hover instead
  • --warning-light → Use --warning (theme handles light/dark)
  • --purple-dark → Use --purple-hover instead
  • --danger → Use --error instead

Color Variables Reference

Required Colors (7) - Must Be Defined

Variable Dark Value Light Value Purpose
--bg-base #0f0f1a #e5e7eb Page background
--bg-surface #1a1a2e #ffffff Cards/panels
--bg-elevated #252538 #f3f4f6 Modals/dropdowns
--text-primary #e4e4e7 #1a1a2e Main text
--text-secondary #949494 #525252 Muted text
--accent #24c8db #0e7490 Primary action color
--border-default #3d3d66 #6b7280 Default borders

Optional Colors (~40) - Have Smart Defaults

Text Variants

Variable Purpose
--text-tertiary Even more muted text
--text-inverse Text on accent backgrounds
--text-disabled Disabled element text

Border Variants

Variable Purpose
--border-hover Border on hover
--border-focus Border on focus (usually accent)
--border-subtle Softer borders (list dividers, etc.)

Accent Variants

Variable Purpose
--accent-hover Accent on hover (darker/lighter)
--accent-muted Transparent accent for backgrounds
--accent-border Accent border color

Semantic: Success

Variable Purpose
--success Success state (green)
--success-hover Success on hover
--success-bg Success background (10% opacity)
--success-muted Subtle success background (5%)
--success-border Success border (30% opacity)

Semantic: Error

Variable Purpose
--error Error state (red)
--error-hover Error on hover
--error-bg Error background
--error-muted Subtle error background
--error-border Error border

Semantic: Warning

Variable Purpose
--warning Warning state (amber)
--warning-hover Warning on hover
--warning-bg Warning background
--warning-muted Subtle warning background
--warning-border Warning border

Semantic: Info

Variable Purpose
--info Info state (often same as accent)
--info-hover Info on hover
--info-bg Info background
--info-muted Subtle info background

Special Colors

Variable Purpose
--purple Purple accent (credit cards, etc.)
--purple-hover Purple on hover
--purple-bg Purple background
--purple-muted Subtle purple background
--orange Orange accent
--orange-hover Orange on hover
--orange-bg Orange background

Financial Semantics

Variable Purpose
--income-color Income amounts (green)
--expense-color Expense amounts (red)
--positive-color Positive values
--negative-color Negative values

State Colors

Variable Purpose
--paid-color Paid status (green)
--unpaid-color Unpaid status (red)
--partial-color Partially paid (amber)
--pending-color Pending (yellow)

Theme JSON Format

Schema

{
  "id": "string", // Unique identifier (e.g., "dark", "ocean")
  "name": "string", // Display name (e.g., "Dark", "Ocean Blue")
  "version": "1.0", // Schema version
  "isDark": true, // true = dark theme, false = light theme
  "colors": {
    // 7 required colors (must be defined)
    "bg-base": "#hex",
    "bg-surface": "#hex",
    "bg-elevated": "#hex",
    "text-primary": "#hex",
    "text-secondary": "#hex",
    "accent": "#hex",
    "border-default": "#hex",
    // Optional colors (omit to use defaults)
    "success": "#hex"
    // ... etc
  },
  "meta": {
    // Optional metadata
    "description": "string",
    "author": "string"
  }
}

Minimal Theme Example (7 colors)

{
  "id": "ocean",
  "name": "Ocean",
  "version": "1.0",
  "isDark": true,
  "colors": {
    "bg-base": "#0a1628",
    "bg-surface": "#0f2744",
    "bg-elevated": "#153357",
    "text-primary": "#e0f2fe",
    "text-secondary": "#7dd3fc",
    "accent": "#38bdf8",
    "border-default": "#1e4976"
  },
  "meta": {
    "description": "Deep ocean blue theme",
    "author": "Your Name"
  }
}

All ~40 optional colors will be derived from defaults based on isDark.


Palette Visualization

Terminal Command

make show-palette              # Shows dark theme (default)
make show-palette theme=light  # Shows light theme
make show-palette theme=compare # Side-by-side comparison

This displays actual colored swatches in your terminal using ANSI escape codes.

Note: Run in your terminal directly, not through OpenCode's output panel.

What You'll See

══════════════════════════════════════════════════════════════════════
  DARK THEME PALETTE
══════════════════════════════════════════════════════════════════════

  BACKGROUNDS
  ████  #0f0f1a  --bg-base         Page background
  ████  #1a1a2e  --bg-surface      Cards/panels
  ████  #252538  --bg-elevated     Modals/dropdowns

  TEXT
  ████  #e4e4e7  --text-primary    Main text
  ████  #949494  --text-secondary  Muted text
  ████  #7a7a7a  --text-tertiary   Even more muted
  ...

Creating New Color Profiles

Step 1: Choose Base Type

Decide if your theme is dark (isDark: true) or light (isDark: false). This affects how defaults are derived.

Step 2: Create Minimal JSON

Start with just the 7 required colors in data/themes/mytheme.json:

{
  "id": "mytheme",
  "name": "My Theme",
  "version": "1.0",
  "isDark": true,
  "colors": {
    "bg-base": "#...",
    "bg-surface": "#...",
    "bg-elevated": "#...",
    "text-primary": "#...",
    "text-secondary": "#...",
    "accent": "#...",
    "border-default": "#..."
  }
}

Step 3: Override Optional Colors (As Needed)

Add optional colors only if you want different values from defaults:

{
  "colors": {
    // ... required colors ...
    "success": "#00ff88", // Custom success color
    "income-color": "#00ff88" // Match success
  }
}

Step 4: Validate

import { validateTheme } from './src/lib/theme/validator';
import { loadTheme } from './src/lib/theme/loader';

const result = validateTheme(myThemeJson);
if (!result.valid) {
  console.error(result.errors);
}

const fullTheme = loadTheme(myThemeJson); // Merges with defaults

Step 5: Test

import { applyTheme } from './src/lib/theme/applier';
applyTheme(fullTheme); // Applies to document.documentElement

Color Theory Fundamentals

Color Harmony Types

Harmony Description Best For
Complementary Opposite on color wheel High contrast accents
Analogous Adjacent colors Cohesive, subtle palettes
Triadic Three evenly spaced Balanced variety
Split-complementary Base + two adjacent to complement Vibrant but balanced

Choosing an Accent Color

  1. Brand alignment - Match your brand identity
  2. Accessibility - Must have sufficient contrast on backgrounds
  3. Emotional resonance - Cyan = modern/tech, Green = growth, Purple = premium

The 60-30-10 Rule

Proportion Role Example
60% Background --bg-base, --bg-surface
30% Secondary --text-secondary, --border-default
10% Accent --accent, semantic colors

Color Derivation Patterns

Hover States

Dark theme:  base color → lighten 10-15%
Light theme: base color → darken 10-15%

Example:

  • --accent: #24c8db--accent-hover: #1ab0c9 (darker)

Muted/Background Variants

accent-muted = rgba(accent, 0.1)
success-bg   = rgba(success, 0.1)
error-border = rgba(error, 0.3)

Semantic Color Families

Each semantic color (success, error, warning, info) has 5 variants:

success family:
  --success        = #4ade80  (primary, for icons/text)
  --success-hover  = #22c55e  (hover state for buttons)
  --success-bg     = rgba(..., 0.1)  (subtle background)
  --success-muted  = rgba(..., 0.05) (very subtle for hover)
  --success-border = rgba(..., 0.3)  (visible but soft)

Naming convention:

  • {color} - Primary color (text, icons, solid elements)
  • {color}-hover - Hover state for buttons/interactive elements
  • {color}-bg - Background fill (~0.1 opacity)
  • {color}-muted - Subtle background for hover states (~0.05 opacity)
  • {color}-border - Border color (~0.3 opacity)

Accessibility & Contrast

WCAG Contrast Requirements

Level Normal Text (< 18pt) Large Text (>= 18pt)
AA (minimum) 4.5:1 3:1
AAA (enhanced) 7:1 4.5:1

Current Dark Theme Contrast Ratios

These ratios have been verified and meet WCAG AA:

Pair Ratio Status
text-primary (#e4e4e7) on bg-base (#0f0f1a) ~13.5:1 Excellent
text-primary on bg-surface (#1a1a2e) ~11.5:1 Excellent
text-primary on bg-elevated (#252538) ~9.5:1 Excellent
text-secondary (#949494) on bg-surface ~5.5:1 Good (AA)
text-tertiary (#7a7a7a) on bg-surface ~4.5:1 Passes AA
accent (#24c8db) on bg-surface ~6.5:1 Good
text-inverse (#000) on accent ~8.5:1 Excellent

Key Pairs to Verify When Changing Colors

Foreground Background Requirement
--text-primary --bg-base Must pass AA (4.5:1)
--text-primary --bg-surface Must pass AA
--text-secondary --bg-surface Must pass AA (~5:1 target)
--text-tertiary --bg-surface Must pass AA (4.5:1 min)
--text-inverse --accent Must pass AA (button text)

Minimum Contrast Guidelines

When choosing new colors, follow these minimum contrast ratios:

Variable Type Minimum Ratio Target Ratio Notes
Primary text 4.5:1 10:1+ Higher is better for readability
Secondary text 4.5:1 5.5:1 Must be clearly readable
Tertiary text 4.5:1 4.5:1 Bare minimum for AA
Disabled text 3:1 3:1 Intentionally low (indicates disabled)
Borders 1.5:1 2:1+ Subtle but visible
Accent on bg 4.5:1 6:1+ For text/icons in accent color

Background Hierarchy

Backgrounds must have sufficient distinction to create visual depth:

Level Variable Dark Value Purpose Min Contrast to Surface
Base --bg-base #0f0f1a Page background 1.3:1
Surface --bg-surface #1a1a2e Cards, panels — (reference)
Elevated --bg-elevated #252538 Modals, dropdowns 1.3:1

Tip: If elevated elements look too similar to surface, increase lightness or use shadows/borders for distinction.

Contrast Calculation

Relative luminance formula:

L = 0.2126 * R + 0.7152 * G + 0.0722 * B
Contrast = (L1 + 0.05) / (L2 + 0.05)

Tools:


Common Pitfalls & Lessons Learned

Pitfall 1: Using -dark or -light Suffixes

Wrong:

color: var(--success-dark);
color: var(--warning-light);

Right:

color: var(--success-hover); /* For hover states */
color: var(--warning); /* Theme profiles handle light/dark */

Why: The theme system has separate dark and light profiles in defaults.ts. Each profile defines appropriate colors for its context. Using -dark/-light suffixes creates ambiguity and breaks when switching themes.

Pitfall 2: Inline rgba() Fallbacks

Wrong:

background: var(--success-muted, rgba(74, 222, 128, 0.05));

Right:

background: var(--success-muted);

Why: If a variable needs a fallback, define it in defaults.ts. Inline fallbacks create maintenance burden and can become out of sync.

Pitfall 3: Insufficient Text Contrast

Common mistake: Using gray values that are too dark for secondary/tertiary text.

Variable Bad Value Good Value Issue
text-secondary #888888 #949494 Too low contrast (~4.8:1 → ~5.5:1)
text-tertiary #666666 #7a7a7a Fails WCAG AA (~3.2:1 → ~4.5:1)

Rule of thumb: On dark backgrounds, secondary text should be at least #909090 or lighter.

Pitfall 4: Glaring Warning Colors

Common mistake: Using highly saturated yellows that cause eye strain.

Bad Good Issue
#fbbf24 #e5a91f Too bright/saturated, causes glare
#f59e0b #d4970f Same issue for hover state

Rule of thumb: Desaturate warning/yellow colors slightly for dark themes.

Pitfall 5: Indistinguishable Background Levels

Common mistake: Background hierarchy colors too similar.

Level Bad Gap Good Gap
base → surface 1.15:1 1.3:1+
surface → elevated 1.1:1 1.25:1+

Solution: If backgrounds look too similar:

  1. Increase lightness difference (preferred)
  2. Add subtle borders between levels
  3. Use shadows for elevated elements

Pitfall 6: Hardcoded Colors in Components

Wrong:

<style>
  .card {
    background: #1a1a2e;
  }
</style>

Right:

<style>
  .card {
    background: var(--bg-surface);
  }
</style>

Detection: Run this command to find hardcoded colors:

rg '#[0-9a-fA-F]{3,8}' -g '*.svelte' src/ --no-heading

Adding New Color Variables

Step 1: Define in types.ts

Add to OPTIONAL_COLOR_KEYS array:

export const OPTIONAL_COLOR_KEYS = [
  // ... existing keys ...
  'my-new-color',
  'my-new-color-hover',
  'my-new-color-bg',
] as const;

Step 2: Add defaults in defaults.ts

Add values for BOTH dark and light themes:

// In DARK_THEME_COLORS:
'my-new-color': '#...',
'my-new-color-hover': '#...',
'my-new-color-bg': 'rgba(..., 0.1)',

// In LIGHT_THEME_COLORS:
'my-new-color': '#...',
'my-new-color-hover': '#...',
'my-new-color-bg': 'rgba(..., 0.1)',

Step 3: Verify Contrast

Check that your new color meets WCAG AA on all background levels:

  • On --bg-base
  • On --bg-surface
  • On --bg-elevated

Step 4: Update Documentation

Add to:

  • docs/colour-themes.md - User-facing docs
  • This skill file - Developer reference

Financial Colors

Variable Color Psychology
--income-color Green Growth, positive, "go" signal
--expense-color Red Caution, negative, attention
--positive-color Green Universal positive association
--negative-color Red Universal negative association

Cultural note: Green/red associations are Western-centric. Some cultures reverse these meanings.

State Colors

State Color Reason
Paid Green Completion, success
Unpaid Red Attention needed, urgent
Partial Amber/Orange In-progress, caution
Pending Yellow Awaiting action, neutral alert

Component Usage Rules

Hard Rule (from AGENTS.md)

NO HARDCODED COLORS: Never use hardcoded hex colors in CSS. Always use CSS variables from the theme system.

Correct Usage

/* Good - Always use variables */
.card {
  background: var(--bg-surface);
  color: var(--text-primary);
  border: 1px solid var(--border-default);
}

.button-primary {
  background: var(--accent);
  color: var(--text-inverse);
}

.button-primary:hover {
  background: var(--accent-hover);
}

.income-amount {
  color: var(--income-color);
}

.expense-amount {
  color: var(--expense-color);
}

Incorrect Usage

/* Bad - Never hardcode */
.card {
  background: #1a1a2e; /* DON'T DO THIS */
  color: #e4e4e7; /* DON'T DO THIS */
}

Quick Reference

Files to Modify

Task File(s)
Change default colors src/lib/theme/defaults.ts
Add new color variable src/lib/theme/types.ts (add to arrays) + defaults.ts
Create built-in theme src/lib/theme/themes/*.json
Create user theme data/themes/*.json

Theme Store API

import { themeMode, currentTheme, isDarkTheme } from '../stores/theme';

$themeMode; // 'dark' | 'light' | 'system'
$isDarkTheme; // boolean
$currentTheme; // Full Theme object

themeMode.set('light'); // Change theme

Programmatic Theme Creation

import { createTheme, loadTheme, applyTheme } from './src/lib/theme';

// Create from scratch
const theme = createTheme({
  id: 'custom',
  name: 'Custom',
  isDark: true,
  colors: {
    /* required colors */
  },
});

// Load from JSON
const theme = loadTheme(jsonObject);

// Apply to DOM
applyTheme(theme);

Old → New Color Mapping

For migrating hardcoded colors:

Old Hardcoded New Variable
#0f0f1a var(--bg-base)
#1a1a2e var(--bg-surface)
#252538 var(--bg-elevated)
#e4e4e7 var(--text-primary)
#949494 var(--text-secondary)
#7a7a7a var(--text-tertiary)
#24c8db var(--accent)
#1ab0c9 var(--accent-hover)
rgba(36, 200, 219, 0.1) var(--accent-muted)
#3d3d66 var(--border-default)
#4ade80, #22c55e var(--success)
#f87171, #ff6b6b, #ef4444 var(--error)
#e5a91f var(--warning)
#000, #000000 var(--text-inverse)

Quick Checklist

Before Committing Color Changes

  • No hardcoded hex colors in .svelte files
  • All new variables follow naming convention (-hover, -bg, -muted, -border)
  • Values defined in BOTH DARK_THEME_COLORS and LIGHT_THEME_COLORS
  • Text colors meet WCAG AA contrast (4.5:1 minimum)
  • Secondary text is at least #909090 on dark backgrounds
  • Tertiary text is at least #7a7a7a on dark backgrounds
  • No -dark or -light suffixes used
  • No inline rgba() fallbacks
  • Documentation updated if new variables added

Contrast Quick Check

Run in browser DevTools console to check a color pair:

// Calculate contrast ratio
function contrast(hex1, hex2) {
  const lum = (hex) => {
    const rgb = hex.match(/\w\w/g).map((x) => parseInt(x, 16) / 255);
    const [r, g, b] = rgb.map((c) =>
      c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)
    );
    return 0.2126 * r + 0.7152 * g + 0.0722 * b;
  };
  const L1 = lum(hex1),
    L2 = lum(hex2);
  return ((Math.max(L1, L2) + 0.05) / (Math.min(L1, L2) + 0.05)).toFixed(2);
}

// Example: text-secondary on bg-surface
contrast('949494', '1a1a2e'); // Should be >= 4.5

Find Hardcoded Colors

# Find all hardcoded hex colors in Svelte files
rg '#[0-9a-fA-F]{3,8}' -g '*.svelte' src/ --no-heading

# Find all CSS variables currently in use
rg 'var\(--[a-z-]+\)' -g '*.svelte' src/ -o | sed 's/.*var(--//' | sed 's/)$//' | sort -u