local-link/skills/css-dev-skills/skills/css-theme/SKILL.md
Build theming systems with modern CSS. Covers custom properties architecture (primitive, semantic, component tokens), oklch/oklab color spaces, color-mix() for tints and shades, light-dark() for automatic dark mode, @property for typed custom properties, multiple theme support, contrast themes, and forced-colors mode. Use when building a theme, adding dark mode, creating color systems, setting up design tokens, or implementing color schemes.
npx skillsauth add lionad-morotar/local-tools css-themeInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are a CSS theming specialist. Your job is to design and implement complete theming systems using modern CSS — no JavaScript theme toggling, no CSS-in-JS, no preprocessor variables. Pure CSS custom properties, color-mix(), light-dark(), and @property.
For the full pattern catalog, see the css-expert skill's modern-patterns.md. For browser support details on light-dark(), @property, and color functions, see browser-compat.md.
Theme Build Progress:
- [ ] Step 1: Define primitive color tokens (oklch palette)
- [ ] Step 2: Create semantic token layer
- [ ] Step 3: Wire up light/dark mode
- [ ] Step 4: Add component-level tokens
- [ ] Step 5: Register animated properties with @property
- [ ] Step 6: Add contrast and forced-colors support
- [ ] Step 7: (Optional) Add extra themes beyond light/dark
Build the raw color palette using oklch(). Every color starts here.
:root {
/* Primary */
--primary-50: oklch(95% 0.05 250);
--primary-100: oklch(90% 0.08 250);
--primary-200: oklch(80% 0.12 250);
--primary-300: oklch(70% 0.18 250);
--primary-400: oklch(60% 0.22 250);
--primary-500: oklch(55% 0.25 250);
--primary-600: oklch(48% 0.22 250);
--primary-700: oklch(40% 0.18 250);
--primary-800: oklch(32% 0.14 250);
--primary-900: oklch(25% 0.10 250);
/* Neutral */
--neutral-0: oklch(100% 0 0);
--neutral-50: oklch(97% 0.005 250);
--neutral-100: oklch(93% 0.005 250);
--neutral-200: oklch(87% 0.01 250);
--neutral-300: oklch(78% 0.01 250);
--neutral-400: oklch(65% 0.01 250);
--neutral-500: oklch(55% 0.01 250);
--neutral-600: oklch(44% 0.01 250);
--neutral-700: oklch(35% 0.01 250);
--neutral-800: oklch(25% 0.01 250);
--neutral-900: oklch(18% 0.01 250);
--neutral-1000: oklch(10% 0.005 0);
}
Instead of hardcoding every shade, derive them:
:root {
--primary: oklch(55% 0.25 250);
--primary-light: color-mix(in oklch, var(--primary), white 40%);
--primary-dark: color-mix(in oklch, var(--primary), black 30%);
--primary-muted: color-mix(in oklch, var(--primary), transparent 60%);
--primary-subtle: color-mix(in oklch, var(--primary), var(--color-surface) 85%);
}
Use color-mix() for hover/active states, overlays, and surface tints rather than separate hardcoded values.
Map primitives to purpose-driven names. These are the tokens components actually consume.
:root {
color-scheme: light dark;
/* Surfaces */
--color-surface: light-dark(var(--neutral-0), var(--neutral-900));
--color-surface-raised: light-dark(var(--neutral-0), var(--neutral-800));
--color-surface-sunken: light-dark(var(--neutral-50), var(--neutral-1000));
--color-surface-overlay: light-dark(
oklch(100% 0 0 / 0.8),
oklch(15% 0 0 / 0.8)
);
/* Text */
--color-text: light-dark(var(--neutral-900), var(--neutral-100));
--color-text-muted: light-dark(var(--neutral-600), var(--neutral-400));
--color-text-subtle: light-dark(var(--neutral-500), var(--neutral-500));
/* Borders */
--color-border: light-dark(var(--neutral-200), var(--neutral-700));
--color-border-strong: light-dark(var(--neutral-400), var(--neutral-500));
/* Interactive */
--color-primary: light-dark(var(--primary-500), var(--primary-400));
--color-primary-hover: light-dark(var(--primary-600), var(--primary-300));
--color-primary-text: light-dark(var(--neutral-0), var(--neutral-900));
/* Status */
--color-success: light-dark(oklch(50% 0.18 150), oklch(70% 0.18 150));
--color-warning: light-dark(oklch(55% 0.18 85), oklch(75% 0.18 85));
--color-error: light-dark(oklch(50% 0.22 25), oklch(70% 0.20 25));
--color-info: light-dark(oklch(50% 0.15 250), oklch(70% 0.15 250));
}
--{color}-{shade} (e.g., --primary-500)--color-{purpose} (e.g., --color-surface, --color-text)--_{property} with underscore prefix (e.g., --_bg, --_border)The light-dark() function reads color-scheme and picks the correct value automatically. The color-scheme declaration on :root is required:
:root {
color-scheme: light dark;
}
This single declaration enables the browser's native light/dark toggle. The user's OS preference drives it via prefers-color-scheme.
Allow users to force a theme with a data attribute:
:root, [data-theme="light"] {
color-scheme: light;
}
[data-theme="dark"] {
color-scheme: dark;
}
The light-dark() values in semantic tokens automatically respond to the color-scheme change — no need to redeclare every variable.
document.documentElement.dataset.theme =
document.documentElement.dataset.theme === "dark" ? "light" : "dark";
Store preference in localStorage. On load, set the attribute before first paint to avoid flash.
Components should use internal custom properties (underscore prefix) mapped to semantic tokens:
.button {
--_bg: var(--color-primary);
--_color: var(--color-primary-text);
--_border: transparent;
--_radius: var(--radius-m);
background: var(--_bg);
color: var(--_color);
border: 1px solid var(--_border);
border-radius: var(--_radius);
&:hover {
--_bg: var(--color-primary-hover);
}
&[data-variant="outline"] {
--_bg: transparent;
--_color: var(--color-primary);
--_border: var(--color-primary);
}
&[data-variant="ghost"] {
--_bg: transparent;
--_color: var(--color-primary);
}
}
This pattern lets variants, states, and themes override internal tokens without touching property declarations.
Register custom properties to enable transitions and type checking:
@property --gradient-angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
@property --color-primary-l {
syntax: "<percentage>";
inherits: true;
initial-value: 55%;
}
--gradient-angle to spin a gradient<length>, <color>, <number>, etc.initial-value acts as a guaranteed fallback@property --angle {
syntax: "<angle>";
inherits: false;
initial-value: 0deg;
}
.gradient-border {
background: conic-gradient(from var(--angle), var(--primary-400), var(--primary-600), var(--primary-400));
transition: --angle 500ms ease;
&:hover {
--angle: 180deg;
}
}
Increase visual distinction for users who request it:
@media (prefers-contrast: more) {
:root {
--color-text: light-dark(oklch(5% 0 0), oklch(98% 0 0));
--color-border: light-dark(oklch(30% 0 0), oklch(80% 0 0));
--color-text-muted: light-dark(oklch(30% 0 0), oklch(80% 0 0));
}
}
Support Windows High Contrast mode. System colors replace your custom colors:
@media (forced-colors: active) {
.button {
border: 1px solid ButtonText;
}
.card {
border: 1px solid CanvasText;
}
.badge {
outline: 1px solid;
}
}
Key system color keywords: Canvas, CanvasText, LinkText, ButtonFace, ButtonText, Highlight, HighlightText, GrayText.
Rules in forced-colors mode:
background-image is removed — don't rely on it for meaningFor brand themes, seasonal themes, or user-customizable themes, use data attributes with full token overrides:
[data-theme="ocean"] {
color-scheme: dark;
--primary-500: oklch(60% 0.20 230);
--color-surface: oklch(18% 0.02 230);
--color-text: oklch(90% 0.01 230);
}
[data-theme="forest"] {
color-scheme: dark;
--primary-500: oklch(55% 0.18 150);
--color-surface: oklch(15% 0.02 150);
--color-text: oklch(90% 0.01 150);
}
Override only the primitives — semantic tokens that reference primitives via var() update automatically.
Expose a small set of properties for user control:
:root {
--user-hue: 250;
--primary-500: oklch(55% 0.25 var(--user-hue));
}
Set --user-hue from JavaScript based on user preference.
When building a theme system, deliver:
tokens.css)@layer blocks or data-attribute overridesprefers-contrast and forced-colors overridescolor-scheme: light dark on :root when using light-dark()oklch() for all color definitions, never hex/rgb/hslcolor-mix(in oklch, ...) for derived colors, not manually computed values--_bg) to signal internal scope--color- prefix for discoverabilitytools
open understand dashboard for user
tools
这是一个技能文件的模板,展示了技能的基本结构和内容组织方式。
development
Be direct and informative. No filler, no fluff, but give enough to be useful.
development
使用 Evaluator-optimizer 模式进行系统性多轮网络搜索,采用结构化 Ask 流程在搜索前澄清研究目标。基于 YC Office Hours 的提问方法论,确保搜索方向清晰、结果可验证。当用户需要深入调查复杂主题、验证假设或全面收集信息时使用。