color-theory
Use this skill when choosing color palettes, ensuring contrast compliance, implementing dark mode, or defining semantic color tokens. Triggers on color palette, contrast ratio, WCAG color, dark mode, color tokens, HSL, OKLCH, brand colors, color harmony, and any task requiring color system design or implementation.
design colorpalettecontrastdark-modetokensaccessibilityWhat is color-theory?
Use this skill when choosing color palettes, ensuring contrast compliance, implementing dark mode, or defining semantic color tokens. Triggers on color palette, contrast ratio, WCAG color, dark mode, color tokens, HSL, OKLCH, brand colors, color harmony, and any task requiring color system design or implementation.
color-theory
color-theory is a production-ready AI agent skill for claude-code, gemini-cli, openai-codex. Choosing color palettes, ensuring contrast compliance, implementing dark mode, or defining semantic color tokens.
Quick Facts
| Field | Value |
|---|---|
| Category | design |
| Version | 0.1.0 |
| Platforms | claude-code, gemini-cli, openai-codex |
| License | MIT |
How to Install
- Make sure you have Node.js installed on your machine.
- Run the following command in your terminal:
npx skills add AbsolutelySkilled/AbsolutelySkilled --skill color-theory- The color-theory skill is now available in your AI coding agent (Claude Code, Gemini CLI, OpenAI Codex, etc.).
Overview
A focused, opinionated guide to building production color systems. Not art school theory - the engineering decisions that determine whether a color system scales, stays accessible, and survives dark mode. Every recommendation ships with working CSS so you can copy-paste into real projects.
Color systems fail in predictable ways: too many hues, raw hex values scattered through components, contrast ratios never checked, dark mode slapped on at the end. This skill prevents all four failure modes with concrete patterns.
Tags
color palette contrast dark-mode tokens accessibility
Platforms
- claude-code
- gemini-cli
- openai-codex
Related Skills
Pair color-theory with these complementary skills:
Frequently Asked Questions
What is color-theory?
Use this skill when choosing color palettes, ensuring contrast compliance, implementing dark mode, or defining semantic color tokens. Triggers on color palette, contrast ratio, WCAG color, dark mode, color tokens, HSL, OKLCH, brand colors, color harmony, and any task requiring color system design or implementation.
How do I install color-theory?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill color-theory in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support color-theory?
This skill works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.
Maintainers
Generated from AbsolutelySkilled
SKILL.md
Color Theory
A focused, opinionated guide to building production color systems. Not art school theory - the engineering decisions that determine whether a color system scales, stays accessible, and survives dark mode. Every recommendation ships with working CSS so you can copy-paste into real projects.
Color systems fail in predictable ways: too many hues, raw hex values scattered through components, contrast ratios never checked, dark mode slapped on at the end. This skill prevents all four failure modes with concrete patterns.
When to use this skill
Trigger this skill when the user:
- Needs to create or extend a brand color palette
- Asks about WCAG contrast ratios or accessibility for color
- Wants to implement dark mode or a light/dark theme switcher
- Needs to define a semantic color token system
- Asks about HSL, OKLCH, or CSS color functions
- Wants to choose harmonious accent or secondary colors
- Needs colors for data visualization or charts
- Asks which color to use for success, error, warning, or info states
Do NOT trigger this skill for:
- Logo design, brand identity strategy, or visual brand decisions not expressed in code
- Layout, spacing, or typography questions (use
ultimate-uifor those)
Key principles
OKLCH over HSL for perceptual uniformity -
hsl(243, 80%, 50%)andhsl(60, 80%, 50%)claim the same lightness but look completely different in brightness to the human eye. OKLCH'sLchannel is perceptually uniform - if two colors share an OKLCH lightness value, they will appear equally bright. Use OKLCH when generating accessible palettes programmatically; use HSL only as a convenience for rough manual adjustments.Semantic tokens over raw values - Components must never reference
#4f46e5directly. Define a primitive scale (--color-indigo-600: #4f46e5) and semantic aliases on top (--color-action-primary: var(--color-indigo-600)). Swapping themes or adjusting brand colors then requires one edit, not a grep-and-replace across the entire codebase.Contrast ratios are non-negotiable - WCAG AA requires 4.5:1 for normal text and 3:1 for large text (18px+ regular, 14px+ bold). These are the legal minimum in many jurisdictions and the ethical baseline everywhere. Check every text/background pair before shipping, including hover, focus, and disabled states - those states fail just as often as default.
Design for dark mode from the start - Bolting on dark mode at the end destroys contrast relationships. The correct approach: define your full semantic token set, then write dark overrides alongside light defaults. The extra 30 minutes up front saves hours of debugging washed-out text and invisible borders.
Less color is more - A palette of 1 brand hue + 1 tinted neutral + 4 status colors (success/warning/error/info) handles 95% of real product UI. Every additional hue increases cognitive load and the chance of contrast failures. Restraint is a feature.
Core concepts
Color spaces
| Space | Perceptually uniform | Best for |
|---|---|---|
| Hex / RGB | No | Copy-paste from design tools |
| HSL | No | Quick manual adjustment |
| OKLCH | Yes | Programmatic palette generation, accessible contrast |
color-mix() |
Depends on space | Tinting, shading, blending in CSS |
OKLCH channels: L = lightness (0-1), C = chroma (0-0.4), H = hue (0-360).
Color harmony
| Relationship | Hue offset | Use case |
|---|---|---|
| Complementary | +180 deg | High-emphasis CTAs, maximum contrast |
| Analogous | +/-30 deg | Secondary/accent in product UI (recommended) |
| Triadic | +120 deg | Data visualization series |
| Split-complementary | +150 / +210 deg | High contrast without full tension |
Contrast ratios
| Level | Normal text | Large text | UI components |
|---|---|---|---|
| AA (minimum) | 4.5:1 | 3:1 | 3:1 |
| AAA (enhanced) | 7:1 | 4.5:1 | N/A in WCAG 2.x |
Tools: browser DevTools accessibility panel, whocanuse.com, colourcontrast.cc.
Semantic vs primitive tokens
Primitive → --color-indigo-600: #4f46e5
Semantic → --color-action-primary: var(--color-indigo-600)
Component → background: var(--color-action-primary)Primitives define what exists. Semantics define what they mean. Components consume meaning, never raw values.
Common tasks
Generate a color palette from a brand color using OKLCH
Start from the brand hex, convert to OKLCH, then step lightness at equal perceptual intervals while holding chroma and hue roughly constant.
:root {
/* Brand: oklch(0.49 0.22 264) - a mid-indigo */
--color-brand-50: oklch(0.97 0.03 264);
--color-brand-100: oklch(0.93 0.06 264);
--color-brand-200: oklch(0.86 0.10 264);
--color-brand-300: oklch(0.76 0.15 264);
--color-brand-400: oklch(0.64 0.19 264);
--color-brand-500: oklch(0.56 0.22 264); /* base */
--color-brand-600: oklch(0.49 0.22 264); /* primary CTA - 4.7:1 on white */
--color-brand-700: oklch(0.42 0.21 264); /* hover state */
--color-brand-800: oklch(0.33 0.18 264); /* active/pressed */
--color-brand-900: oklch(0.24 0.14 264); /* text on light bg */
/* Tinted neutral - brand hue at low chroma */
--color-neutral-50: oklch(0.98 0.005 264);
--color-neutral-100: oklch(0.95 0.007 264);
--color-neutral-200: oklch(0.90 0.009 264);
--color-neutral-300: oklch(0.82 0.011 264);
--color-neutral-400: oklch(0.68 0.013 264);
--color-neutral-500: oklch(0.54 0.013 264);
--color-neutral-600: oklch(0.43 0.012 264);
--color-neutral-700: oklch(0.33 0.010 264);
--color-neutral-800: oklch(0.22 0.008 264);
--color-neutral-900: oklch(0.14 0.006 264);
}Rule of thumb: primary CTA needs L between 0.45-0.52 for 4.5:1 on white. Check with DevTools before shipping.
Ensure WCAG contrast compliance
Check and fix common failing combinations using the -600 / -400 shift rule:
/* FAILS: gray-400 on white = ~2.7:1 */
.badge-label {
color: var(--color-neutral-400); /* oklch(0.68 ...) */
}
/* PASSES: gray-600 on white = ~5.9:1 */
.badge-label {
color: var(--color-neutral-600); /* oklch(0.43 ...) */
}
/* In dark mode: flip to lighter shades */
[data-theme="dark"] .badge-label {
color: var(--color-neutral-300); /* high L = passes on dark bg */
}/* Focus rings: 3:1 against adjacent colors, not just background */
:focus-visible {
outline: 2px solid var(--color-brand-600);
outline-offset: 2px;
}
/* On dark backgrounds, lighten the ring */
[data-theme="dark"] :focus-visible {
outline-color: var(--color-brand-400);
}Implement dark mode with CSS custom properties
/* 1. Light defaults on :root */
:root {
--color-bg-primary: oklch(0.98 0.005 264);
--color-bg-secondary: oklch(0.95 0.007 264);
--color-bg-elevated: oklch(1.00 0.000 264); /* pure white cards */
--color-text-primary: oklch(0.16 0.010 264);
--color-text-secondary: oklch(0.43 0.012 264);
--color-text-muted: oklch(0.60 0.010 264);
--color-border: oklch(0.88 0.009 264);
--color-border-strong: oklch(0.78 0.011 264);
--color-action-primary: var(--color-brand-600);
--color-action-primary-hover: var(--color-brand-700);
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.08);
--shadow-md: 0 4px 8px oklch(0 0 0 / 0.10);
}
/* 2. Dark overrides - defined alongside, not appended later */
[data-theme="dark"],
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg-primary: oklch(0.19 0.012 264); /* dark blue-gray */
--color-bg-secondary: oklch(0.14 0.010 264); /* deeper layer */
--color-bg-elevated: oklch(0.25 0.013 264); /* cards sit above */
--color-text-primary: oklch(0.93 0.008 264); /* off-white, not pure */
--color-text-secondary: oklch(0.70 0.011 264);
--color-text-muted: oklch(0.52 0.010 264);
--color-border: oklch(0.30 0.013 264);
--color-border-strong: oklch(0.38 0.013 264);
--color-action-primary: var(--color-brand-400); /* lighter in dark */
--color-action-primary-hover: var(--color-brand-300);
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.40);
--shadow-md: 0 4px 8px oklch(0 0 0 / 0.50);
}
}Never use pure
#000000in dark mode backgrounds - it is harsh and eliminates all depth cues. Never use pure#fffffffor text on dark - reduce to L ~0.93 to prevent eye strain.
Define a semantic color token system
:root {
/* ---- Primitive scale (source of truth) ---- */
--color-brand-400: oklch(0.64 0.19 264);
--color-brand-600: oklch(0.49 0.22 264);
--color-brand-700: oklch(0.42 0.21 264);
--color-green-400: oklch(0.73 0.17 145); --color-green-600: oklch(0.53 0.17 145);
--color-red-400: oklch(0.68 0.19 27); --color-red-600: oklch(0.50 0.19 27);
--color-amber-400: oklch(0.80 0.15 70); --color-amber-600: oklch(0.63 0.16 70);
--color-blue-400: oklch(0.67 0.16 232); --color-blue-600: oklch(0.50 0.18 232);
/* ---- Semantic aliases ---- */
--color-action-primary: var(--color-brand-600);
--color-action-primary-hover: var(--color-brand-700);
--color-status-success: var(--color-green-600);
--color-status-warning: var(--color-amber-600);
--color-status-error: var(--color-red-600);
--color-status-info: var(--color-blue-600);
--color-status-success-bg: oklch(0.96 0.04 145);
--color-status-warning-bg: oklch(0.97 0.04 70);
--color-status-error-bg: oklch(0.97 0.03 27);
--color-status-info-bg: oklch(0.96 0.03 232);
}
[data-theme="dark"] {
/* Semantic overrides only - primitives unchanged */
--color-action-primary: var(--color-brand-400);
--color-action-primary-hover: var(--color-brand-300);
--color-status-success: var(--color-green-400);
--color-status-warning: var(--color-amber-400);
--color-status-error: var(--color-red-400);
--color-status-info: var(--color-blue-400);
--color-status-success-bg: oklch(0.22 0.05 145);
--color-status-warning-bg: oklch(0.22 0.05 70);
--color-status-error-bg: oklch(0.22 0.04 27);
--color-status-info-bg: oklch(0.22 0.04 232);
}Create accessible data visualization colors
Data viz colors must be distinguishable by colorblind users (8% of males have red-green deficiency). Use hues spaced 45+ degrees apart in OKLCH hue and vary chroma and lightness too.
:root {
/* 6-series palette - colorblind safe, distinct at equal lightness */
--chart-1: oklch(0.55 0.20 264); /* blue-violet */
--chart-2: oklch(0.55 0.18 145); /* green */
--chart-3: oklch(0.55 0.20 27); /* red */
--chart-4: oklch(0.55 0.16 70); /* amber */
--chart-5: oklch(0.55 0.18 310); /* purple */
--chart-6: oklch(0.55 0.16 200); /* cyan */
}
/* Never rely on color alone - add pattern/shape redundancy */
.chart-series-1 { stroke: var(--chart-1); stroke-dasharray: none; }
.chart-series-2 { stroke: var(--chart-2); stroke-dasharray: 6 3; }
.chart-series-3 { stroke: var(--chart-3); stroke-dasharray: 2 3; }Use a tool like
Oklab Palette GeneratororHuemintto verify colorblind simulations. Never use red + green as the only distinguishing pair.
Use CSS color-mix() for tints and shades
/* Tint: mix brand with white */
.alert-info-bg {
background: color-mix(in oklch, var(--color-brand-600) 15%, white);
}
/* Shade: mix with black */
.btn-primary:active {
background: color-mix(in oklch, var(--color-brand-600) 85%, black);
}
/* Overlay with opacity */
.overlay {
background: color-mix(in oklch, var(--color-brand-600) 8%, transparent);
}
/* Generate hover dynamically without extra token */
.tag:hover {
background: color-mix(in oklch, var(--color-action-primary) 12%, var(--color-bg-primary));
}
color-mix()is supported in all modern browsers (Chrome 111+, Firefox 113+, Safari 16.2+). Always specify the color space -in oklchgives perceptually smooth results.
Choose harmonious accent colors
Derive accents from your brand hue using OKLCH offsets. For an indigo brand at hue 264:
:root {
--color-brand-600: oklch(0.49 0.22 264);
--color-accent-complement: oklch(0.63 0.16 84); /* +180 - amber, max contrast */
--color-accent-analogous: oklch(0.52 0.21 294); /* +30 - purple, cohesive */
}Analogous (+30 deg) is the safest choice for product UI. Use the complementary accent (+180 deg) only for high-emphasis CTAs where you need maximum contrast against the brand.
Anti-patterns
| Anti-pattern | Why it fails | Correct approach |
|---|---|---|
| Raw hex in components | Cannot theme, breaks dark mode, causes search-replace nightmares | Always use semantic tokens: var(--color-action-primary) |
Pure black on white #000 / #fff |
Extreme contrast causes halation; looks unnatural on screens | Use oklch(0.13 0.01 264) on oklch(0.98 0.005 264) |
| Gray neutrals with 0 chroma | Feels clinical and disconnected from brand | Add 3-5% brand chroma to all neutrals: oklch(L 0.008 264) |
| Checking contrast only in light mode | Dark mode state colors fail just as often | Test every token pair in both light and dark; check hover/focus/disabled states too |
| Using HSL for accessible palette generation | HSL lightness is not perceptual; hsl(60 80% 50%) looks far brighter than hsl(240 80% 50%) despite identical L |
Use OKLCH for any programmatic or accessibility-critical color math |
| Red and green as the only data viz distinction | ~8% of users cannot distinguish them | Add shape/pattern redundancy and use hues that differ by 45+ degrees |
Gotchas
OKLCH browser support for older environments - OKLCH is supported in Chrome 111+, Firefox 113+, and Safari 15.4+. Environments supporting older browsers need a PostCSS plugin (
postcss-oklab-function) or explicit hex fallbacks. Don't use OKLCH directly in production CSS without confirming your browser support matrix or adding a build-time transform.Dark mode contrast inversion failure - A color that passes 4.5:1 in light mode often fails in dark mode because the relationship between text and background flips. A neutral-600 on white-background passes; the same neutral-600 on a dark-800 background may also fail (insufficient contrast in the other direction). Test every semantic token pair in both modes.
color-mix()in oklch produces unexpected hues near red - OKLCH hue wraps at 360. When mixing two colors whose hues straddle 0/360 (e.g., hue 350 and hue 10), the interpolation goes the long way around the hue wheel through greens and blues instead of the short way through near-reds. Specifyhue shorterincolor-mix()to force the short path:color-mix(in oklch shorter hue, ...).Status color background tints failing in dark mode - Light-mode status backgrounds (e.g.,
oklch(0.96 0.04 145)for success) are nearly white - they look fine on white backgrounds but become invisible on dark mode backgrounds. Always define explicit dark-mode overrides for status background tokens rather than letting them inherit.Semantic token naming that doesn't survive rebrand - Naming a token
--color-blue-actioninstead of--color-action-primarymeans a rebrand from blue to teal requires renaming the token everywhere it's used. Semantic tokens should describe purpose (action,status-success,text-muted), never the color value itself.
References
references/palette-recipes.md- Pre-built palette recipes for common product archetypes (SaaS, e-commerce, editorial, fintech)
Only load a references file if the current task requires it - they are long and will consume context.
References
palette-recipes.md
Palette Recipes
Pre-built color systems for common product archetypes. Each recipe includes
a full primitive scale, semantic token map, and dark mode overrides ready to
drop into a :root block. All contrast pairs are WCAG AA compliant.
How to use these recipes
- Pick the recipe closest to your product archetype.
- Replace the brand hue (the OKLCH
Hvalue) with your own brand hue. - Verify primary CTA contrast with browser DevTools before shipping.
- Keep primitives as-is; only customize the semantic layer.
Recipe 1: SaaS / Productivity (Indigo-Neutral)
Calm, trustworthy, familiar. Indigo brand on a cool blue-gray neutral.
/* === PRIMITIVES === */
:root {
/* Brand: indigo (hue 264) */
--color-brand-50: oklch(0.97 0.03 264);
--color-brand-100: oklch(0.93 0.06 264);
--color-brand-200: oklch(0.86 0.10 264);
--color-brand-300: oklch(0.76 0.15 264);
--color-brand-400: oklch(0.64 0.19 264);
--color-brand-500: oklch(0.56 0.22 264);
--color-brand-600: oklch(0.49 0.22 264); /* 4.7:1 on white */
--color-brand-700: oklch(0.42 0.21 264);
--color-brand-800: oklch(0.33 0.18 264);
--color-brand-900: oklch(0.24 0.14 264);
/* Neutral: tinted with brand hue at low chroma */
--color-neutral-50: oklch(0.98 0.005 264);
--color-neutral-100: oklch(0.95 0.007 264);
--color-neutral-200: oklch(0.90 0.009 264);
--color-neutral-300: oklch(0.82 0.011 264);
--color-neutral-400: oklch(0.68 0.012 264);
--color-neutral-500: oklch(0.54 0.012 264);
--color-neutral-600: oklch(0.43 0.011 264);
--color-neutral-700: oklch(0.33 0.010 264);
--color-neutral-800: oklch(0.22 0.008 264);
--color-neutral-900: oklch(0.14 0.006 264);
}
/* === SEMANTICS - LIGHT === */
:root {
--color-bg-primary: var(--color-neutral-50);
--color-bg-secondary: var(--color-neutral-100);
--color-bg-elevated: oklch(1.00 0.000 264);
--color-text-primary: var(--color-neutral-900);
--color-text-secondary: var(--color-neutral-600);
--color-text-muted: var(--color-neutral-400);
--color-border: var(--color-neutral-200);
--color-border-strong: var(--color-neutral-300);
--color-action-primary: var(--color-brand-600);
--color-action-primary-hover: var(--color-brand-700);
--color-action-primary-text: oklch(1.00 0.000 264);
--color-action-secondary: transparent;
--color-action-secondary-border: var(--color-brand-200);
--color-action-secondary-hover: var(--color-brand-50);
--color-status-success: oklch(0.53 0.17 145);
--color-status-success-bg: oklch(0.96 0.04 145);
--color-status-warning: oklch(0.63 0.16 70);
--color-status-warning-bg: oklch(0.97 0.04 70);
--color-status-error: oklch(0.50 0.19 27);
--color-status-error-bg: oklch(0.97 0.03 27);
--color-status-info: var(--color-brand-600);
--color-status-info-bg: var(--color-brand-50);
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.07);
--shadow-md: 0 4px 8px oklch(0 0 0 / 0.09);
--shadow-lg: 0 12px 24px oklch(0 0 0 / 0.11);
}
/* === SEMANTICS - DARK === */
[data-theme="dark"],
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg-primary: oklch(0.20 0.012 264);
--color-bg-secondary: oklch(0.15 0.010 264);
--color-bg-elevated: oklch(0.26 0.014 264);
--color-text-primary: oklch(0.93 0.008 264);
--color-text-secondary: oklch(0.70 0.010 264);
--color-text-muted: oklch(0.52 0.010 264);
--color-border: oklch(0.31 0.012 264);
--color-border-strong: oklch(0.39 0.012 264);
--color-action-primary: var(--color-brand-400);
--color-action-primary-hover: var(--color-brand-300);
--color-action-primary-text: oklch(0.14 0.006 264);
--color-action-secondary-border: var(--color-brand-700);
--color-action-secondary-hover: oklch(0.26 0.014 264);
--color-status-success: oklch(0.73 0.17 145);
--color-status-success-bg: oklch(0.22 0.05 145);
--color-status-warning: oklch(0.80 0.15 70);
--color-status-warning-bg: oklch(0.22 0.05 70);
--color-status-error: oklch(0.68 0.19 27);
--color-status-error-bg: oklch(0.22 0.04 27);
--color-status-info: var(--color-brand-400);
--color-status-info-bg: oklch(0.22 0.04 264);
--shadow-sm: 0 1px 2px oklch(0 0 0 / 0.40);
--shadow-md: 0 4px 8px oklch(0 0 0 / 0.50);
--shadow-lg: 0 12px 24px oklch(0 0 0 / 0.60);
}
}Recipe 2: E-Commerce / Consumer (Emerald-Warm)
Friendly, energetic, conversion-focused. Green brand signals freshness and "go". Warm neutrals feel approachable.
/* === PRIMITIVES === */
:root {
/* Brand: emerald-green (hue 160) */
--color-brand-50: oklch(0.97 0.04 160);
--color-brand-100: oklch(0.93 0.07 160);
--color-brand-200: oklch(0.86 0.12 160);
--color-brand-300: oklch(0.76 0.16 160);
--color-brand-400: oklch(0.65 0.19 160);
--color-brand-500: oklch(0.57 0.20 160);
--color-brand-600: oklch(0.50 0.20 160); /* 4.6:1 on white */
--color-brand-700: oklch(0.42 0.18 160);
--color-brand-800: oklch(0.33 0.15 160);
--color-brand-900: oklch(0.24 0.11 160);
/* Neutral: warm beige-gray (hue 60 = warm direction) */
--color-neutral-50: oklch(0.98 0.006 60);
--color-neutral-100: oklch(0.95 0.008 60);
--color-neutral-200: oklch(0.90 0.010 60);
--color-neutral-300: oklch(0.82 0.012 60);
--color-neutral-400: oklch(0.68 0.013 60);
--color-neutral-500: oklch(0.54 0.013 60);
--color-neutral-600: oklch(0.43 0.012 60);
--color-neutral-700: oklch(0.33 0.010 60);
--color-neutral-800: oklch(0.22 0.008 60);
--color-neutral-900: oklch(0.14 0.006 60);
/* Accent: amber for sale tags, CTAs (complementary to green) */
--color-accent-400: oklch(0.80 0.16 70);
--color-accent-600: oklch(0.63 0.17 70);
}
/* === SEMANTICS - LIGHT === */
:root {
--color-bg-primary: var(--color-neutral-50);
--color-bg-secondary: var(--color-neutral-100);
--color-bg-elevated: oklch(1.00 0.000 60);
--color-text-primary: var(--color-neutral-900);
--color-text-secondary: var(--color-neutral-600);
--color-text-muted: var(--color-neutral-400);
--color-border: var(--color-neutral-200);
--color-action-primary: var(--color-brand-600);
--color-action-primary-hover: var(--color-brand-700);
/* Sale / promo accent */
--color-promo: var(--color-accent-600);
--color-promo-bg: oklch(0.97 0.05 70);
}
/* === SEMANTICS - DARK === */
[data-theme="dark"],
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg-primary: oklch(0.18 0.010 60);
--color-bg-secondary: oklch(0.13 0.008 60);
--color-bg-elevated: oklch(0.24 0.012 60);
--color-text-primary: oklch(0.94 0.006 60);
--color-text-secondary: oklch(0.70 0.009 60);
--color-text-muted: oklch(0.52 0.009 60);
--color-border: oklch(0.30 0.010 60);
--color-action-primary: var(--color-brand-400);
--color-action-primary-hover: var(--color-brand-300);
--color-promo: var(--color-accent-400);
--color-promo-bg: oklch(0.22 0.05 70);
}
}Recipe 3: Editorial / Content (Slate-Serif)
Minimal, typographic, high contrast. Near-black on near-white. A single accent for links and interactive elements. Works equally well for blogs, documentation, and news sites.
/* === PRIMITIVES === */
:root {
/* Brand: slate-blue accent (hue 220) */
--color-brand-400: oklch(0.63 0.17 220);
--color-brand-600: oklch(0.48 0.18 220); /* 5.1:1 on white */
--color-brand-700: oklch(0.40 0.17 220);
/* Neutral: near-neutral with subtle cool tint */
--color-neutral-50: oklch(0.99 0.003 220);
--color-neutral-100: oklch(0.96 0.005 220);
--color-neutral-200: oklch(0.91 0.007 220);
--color-neutral-300: oklch(0.83 0.009 220);
--color-neutral-400: oklch(0.69 0.010 220);
--color-neutral-500: oklch(0.55 0.010 220);
--color-neutral-600: oklch(0.44 0.009 220);
--color-neutral-700: oklch(0.34 0.008 220);
--color-neutral-800: oklch(0.23 0.007 220);
--color-neutral-900: oklch(0.13 0.005 220);
}
/* === SEMANTICS - LIGHT === */
:root {
--color-bg-primary: var(--color-neutral-50);
--color-bg-secondary: var(--color-neutral-100);
--color-bg-elevated: oklch(1.00 0.000 220);
--color-text-primary: var(--color-neutral-900); /* 16:1 on bg */
--color-text-secondary: var(--color-neutral-600);
--color-text-muted: var(--color-neutral-400);
--color-border: var(--color-neutral-200);
/* Links and interactive */
--color-link: var(--color-brand-600);
--color-link-hover: var(--color-brand-700);
/* Blockquote / pull quote accent */
--color-accent-bar: var(--color-brand-600);
--color-accent-bar-bg: var(--color-brand-400);
}
/* === SEMANTICS - DARK === */
[data-theme="dark"],
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg-primary: oklch(0.16 0.007 220);
--color-bg-secondary: oklch(0.12 0.005 220);
--color-bg-elevated: oklch(0.21 0.008 220);
--color-text-primary: oklch(0.94 0.005 220);
--color-text-secondary: oklch(0.70 0.008 220);
--color-text-muted: oklch(0.53 0.008 220);
--color-border: oklch(0.28 0.008 220);
--color-link: var(--color-brand-400);
--color-link-hover: oklch(0.74 0.16 220);
}
}Recipe 4: Fintech / Enterprise (Navy-Gold)
Authoritative, serious, trustworthy. Deep navy signals stability. Gold accent communicates premium value. Zero tolerance for contrast failures.
/* === PRIMITIVES === */
:root {
/* Brand: deep navy (hue 245) */
--color-brand-50: oklch(0.97 0.02 245);
--color-brand-100: oklch(0.93 0.05 245);
--color-brand-200: oklch(0.85 0.09 245);
--color-brand-300: oklch(0.73 0.14 245);
--color-brand-400: oklch(0.60 0.18 245);
--color-brand-500: oklch(0.50 0.20 245);
--color-brand-600: oklch(0.42 0.20 245);
--color-brand-700: oklch(0.35 0.18 245);
--color-brand-800: oklch(0.27 0.14 245);
--color-brand-900: oklch(0.18 0.10 245);
/* Gold accent (analogous to brand, shifted toward warm amber) */
--color-gold-300: oklch(0.84 0.14 78);
--color-gold-500: oklch(0.73 0.16 78);
--color-gold-600: oklch(0.63 0.17 78); /* 3.2:1 on white - use on dark bg only */
--color-gold-700: oklch(0.54 0.16 78); /* 4.7:1 on white */
/* Neutral: cool gray leaning toward brand navy */
--color-neutral-50: oklch(0.98 0.004 245);
--color-neutral-100: oklch(0.95 0.006 245);
--color-neutral-200: oklch(0.90 0.008 245);
--color-neutral-300: oklch(0.82 0.010 245);
--color-neutral-400: oklch(0.68 0.011 245);
--color-neutral-500: oklch(0.54 0.011 245);
--color-neutral-600: oklch(0.43 0.010 245);
--color-neutral-700: oklch(0.33 0.009 245);
--color-neutral-800: oklch(0.22 0.007 245);
--color-neutral-900: oklch(0.14 0.005 245);
}
/* === SEMANTICS - LIGHT === */
:root {
--color-bg-primary: var(--color-neutral-50);
--color-bg-secondary: var(--color-neutral-100);
--color-bg-elevated: oklch(1.00 0.000 245);
/* Navy sidebar / header */
--color-bg-nav: var(--color-brand-900);
--color-text-nav: var(--color-neutral-50);
--color-text-primary: var(--color-neutral-900);
--color-text-secondary: var(--color-neutral-600);
--color-text-muted: var(--color-neutral-400);
--color-border: var(--color-neutral-200);
--color-action-primary: var(--color-brand-600);
--color-action-primary-hover: var(--color-brand-700);
--color-action-primary-text: oklch(1.00 0.000 245);
/* Gold: premium badges, pro features - use on dark bg */
--color-premium: var(--color-gold-600);
--color-premium-bg: oklch(0.97 0.04 78);
--color-status-success: oklch(0.53 0.15 145);
--color-status-error: oklch(0.50 0.18 27);
--color-status-warning: var(--color-gold-700);
}
/* === SEMANTICS - DARK === */
[data-theme="dark"],
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
--color-bg-primary: oklch(0.16 0.010 245);
--color-bg-secondary: oklch(0.12 0.008 245);
--color-bg-elevated: oklch(0.22 0.012 245);
/* Nav stays dark but slightly lighter than deepest bg */
--color-bg-nav: oklch(0.10 0.007 245);
--color-text-primary: oklch(0.94 0.006 245);
--color-text-secondary: oklch(0.70 0.009 245);
--color-text-muted: oklch(0.52 0.009 245);
--color-border: oklch(0.28 0.010 245);
--color-action-primary: var(--color-brand-400);
--color-action-primary-hover: var(--color-brand-300);
--color-action-primary-text: oklch(0.12 0.006 245);
--color-premium: var(--color-gold-500);
--color-premium-bg: oklch(0.22 0.05 78);
--color-status-success: oklch(0.73 0.15 145);
--color-status-error: oklch(0.68 0.18 27);
--color-status-warning: var(--color-gold-300);
}
}Quick contrast-check pairs
Before shipping, verify these pairs in DevTools for each recipe:
| Pair | Light target | Dark target |
|---|---|---|
| Body text on bg-primary | 12:1+ | 12:1+ |
| Secondary text on bg-primary | 5:1+ | 5:1+ |
| Muted text on bg-primary | 3:1+ (large only) | 3:1+ |
| Action primary text on action bg | 4.5:1+ | 4.5:1+ |
| Border on bg-primary | Not required (decorative) | Not required |
| Focus ring on bg-primary | 3:1+ | 3:1+ |
| Status text on status bg | 4.5:1+ | 4.5:1+ |
Changing the brand hue
All four recipes parameterize the brand by OKLCH hue. To change hue:
- Find the OKLCH hue for your brand color using
oklch.comor DevTools. - Replace all
264(or160,220,245) values with your hue. - For the neutral scale, use the same hue at chroma
0.005-0.013. - Re-verify your primary CTA lightness gives 4.5:1 on the bg - adjust L up or down as needed.
Common brand hues for reference:
| Color family | Approx OKLCH hue |
|---|---|
| Red | 25-30 |
| Orange | 50-60 |
| Amber/Gold | 70-80 |
| Yellow-green | 120-130 |
| Green | 140-160 |
| Teal/Cyan | 185-200 |
| Blue | 220-240 |
| Indigo/Violet | 260-280 |
| Purple | 295-310 |
| Magenta/Pink | 330-350 |
Frequently Asked Questions
What is color-theory?
Use this skill when choosing color palettes, ensuring contrast compliance, implementing dark mode, or defining semantic color tokens. Triggers on color palette, contrast ratio, WCAG color, dark mode, color tokens, HSL, OKLCH, brand colors, color harmony, and any task requiring color system design or implementation.
How do I install color-theory?
Run npx skills add AbsolutelySkilled/AbsolutelySkilled --skill color-theory in your terminal. The skill will be immediately available in your AI coding agent.
What AI agents support color-theory?
color-theory works with claude-code, gemini-cli, openai-codex. Install it once and use it across any supported AI coding agent.