dark-mode-token-migration
Add dark mode to a light-only app that styles with RAW color utilities (Tailwind `stone-*`/`slate-*`/`gray-*`) via a semantic token layer, migrating utilities…
Skill by ever-just
Chalk Labs
Need help deploying dark-mode-token-migration?
Chalk Labs sets up agents like this inside real teams: connected to your tools, tested on your data, with a human in the loop where it matters.
Install
Works with Claude Code, Claude apps, Codex, Cursor, Gemini CLI, GitHub Copilot, VS Code, OpenCode, Goose, Windsurf. Read the repository before you give any tool access to your data.
npx agentmarket install skill/ever-just-dark-mode-token-migration
Asks which of your tools to add it to (Claude Code, Codex, Cursor, VS Code, Windsurf, Gemini CLI, Claude Desktop) and shows the change first. See https://agentmart.dev/cli.
Get the weekly diff
The weekly diff: one email a week with new agent tools, notable updates and grade changes, plus security alerts for tools you follow. Double opt-in, one-click unsubscribe.
About
Use cases: design, frontend
Reviews
Write a reviewNo reviews yet. Installed it? Reviews come from verified installers only. How reviews work
Permissions
What this skill can do and what it asks for, from its SKILL.md. Checked Oct 25, 2026.
- Tools
- No tools found
- Secrets
- None asked for
- Install scripts
- None found
Network hosts it talks to 1
codeload.github.com· found in install command
Files it touches
~/.claude/skills/dark-mode-token-migration
What's inside
- Skill name
dark-mode-token-migration- Description
- Add dark mode to a light-only app that styles with RAW color utilities (Tailwind `stone-*`/`slate-*`/`gray-*`) via a semantic token layer, migrating utilities onto it so light stays PIXEL-IDENTICAL and dark comes for free. Use when asked to "add dark mode", or to refactor hardcoded color utilities into a themeable token system. Not for apps already on semantic tokens — just add `.dark` values plus a toggle.
- License
- MIT
- Instructions
- 189 lines in SKILL.md · view on GitHub
Files (3)
- SKILL.md
- mapping-table.md · templates/
- token-layer.css · templates/
SKILL.md (first lines)
# Dark-Mode Token Migration
## Overview
The wrong way to add dark mode is per-page `dark:` variants — you double every color class, drift, and never finish. The right way is a **semantic token layer**: define CSS variables (`--background`, `--foreground`, `--card`, …) once, set them to the app's **exact current palette** in `:root`, override them with an inverted palette in `.dark`, and migrate the app's raw color utilities (`bg-stone-50`, `text-stone-500`) onto the tokens (`bg-background`, `text-muted-foreground`). Because each token's **light value equals the raw value it replaces**, a correct migration is **pixel-identical in light mode** and gains dark mode for free.
This produces four deliverables: a token layer, a no-flash toggle, a per-route utility migration (the parallelizable bulk), and a both-themes visual QA. The discipline that makes it safe is one **invariant** plus one **do-not-touch list** — hold both and the diff is boringly mechanical.
## When to use
- "Add dark mode to the app / dashboard / marketing site." (light-only today, no theme system)
- "Refactor our hardcoded colors into a themeable token system."
- "We style everything with raw `stone-*` / `slate-*` / `gray-*` classes and want a theme switch."
- Any migration from direct color utilities to a `bg-background`/`text-foreground` semantic system.
## When NOT to use this skill
- **The app already uses semantic tokens** (`bg-background`, `text-muted-foreground`, shadcn-style vars). There's nothing to migrate — just add the `.dark { … }` values and a toggle. This skill's whole bulk (the utility migration) doesn't apply.
- **A single component** needs a dark variant. Just write the `dark:` classes inline; don't stand up a token system for one card.
- **You want to also redo the visual design.** Keep migration and redesign separate — migrate to identical light first (prove pixel-parity), redesign after. Mixing them makes "is light broken?" unanswerable. Use `frontend-design` for the redesign pass.
## The invariant that makes this safe
**Every token's LIGHT value MUST equal the raw utility value it replaces.** `--background` in `:root` is literally `stone-50`; `--foreground` is literally `stone-900`. So swapping `bg-stone-50 → bg-background` changes **nothing** visually in light mode — it only adds a `.dark` code path. This gives you a cheap, machine-checkable correctness test: **light mode screenshots before and after must be pixel-identical.** Any light-mode difference means you either picked the wrong token or touched something on the do-not-touch list. Never "improve" a color during migration — that breaks the invariant and hides real regressions.
## Step 1 — the semantic token layer (do this FIRST, single-owner)
One person/agent owns the theme file (`globals.css`). Define every token in `:root` at the current palette, then invert in `.dark`. Keep the **one accent hue** (e.g. red `--destructive`) **identical in both themes** — it's a signal color, not chrome. Set `color-scheme` so native controls (scrollbars, form widgets) follow the theme.
```css
/* globals.css — :root light values EQUAL the current stone palette (pixel-identical).
.dark inverts each one; @theme inline binds them to utilities. Full file below. */
:root {
color-scheme: light;
--background: #fafaf9; /* stone-50 — app canvas */
--foreground: #1c1917; /* stone-900 — primary text */
--card: #ffffff; /* white — panels/inputs (NOT bg)*/
--muted-foreground: #78716c; /* stone-500 — secondary/label text */
--border: #e7e5e4; /* stone-200 — dividers, card edges */
--primary: #1c1917; /* stone-900 — emphasis fill */
--destructive: #dc2626; /* red-600 — the ONE hue, kept in .dark */
/* …full token set (popover/accent/secondary/input/ring/foregrounds) in the template */
}
```
**Tailwind v4** binds each token to a utility with `@theme inline` (e.g. `--color-background: var(--background);`) — that's what makes `bg-background`, `text-muted-foreground`, `border-border`, `ring-ring` exist and resolve at the **use site**, so they follow `.dark` automatically. **Tailwind v3** has no `@theme`: define the vars in `@layer base` and map them into `theme.extend.colors` (`background: "var(--background)"`, `card: { DEFAULT: "var(--card)", foreground: "var(--card-foreground)" }`, …) instead.
Full ready-to-paste file (both `:root`/`.dark` sets + `@theme inline`): **`templates/token-layer.css`**. Commit this layer **before** any utility migration — leaf agents that migrate a route need the tokens to already exist, or they'll invent names that don't resolve.
## Step 2 — the no-flash theme toggle
A theme applied in React (`useEffect`) flashes light-then-dark on every load. Apply it **before paint** with an inline script in the document head, then hydrate.
```tsx
// app/layout.tsx (Next.js App Router) — runs before React hydrates
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script
dangerouslySetInnerHTML={{
// stay LIGHT unless the user EXPLICITLY chose dark
__html: `try{if(localStorage.getItem('theme')==='dark')document.documentElement.classList.add('dark')}catch(e){}`,
}}
/>
</head>
<body>{children}</body>
</html>
);
}
```
- `suppressHydrationWarning` on `<html>` is required — the pre-paint script mutates the class list, which would otherwise trip a hydration mismatch.
- **CRITICAL DEFAULT: stay LIGHT unless the stored value is exactly `'dark'`.** Do **NOT** fall back to `matchMedia('(prefers-color-scheme: dark)')` until the **entire app is migrated**. Auto-adopting the OS preference while un-migrated surfaces still use raw light-only utilities renders the app **half-dark** — a dark shell around white-on-white pages. Flip to `prefers-color-scheme` only as the final step, once every route is tokenized.
The `useTheme` hook reads what the pre-paint script already applied and lets a visible control flip it:
```tsx
// hooks/useTheme.ts
import { useCallback, useEffect, useState } from "react";
export function useTheme() {
const [theme, setTheme] = useState<"light" | "dark">("light");
useEffect(() => {
setTheme(document.documentElement.classList.contains("dark") ? "dark" : "light");
}, []);
const apply = useCallback((next: "light" | "dark") => {
document.documentElement.classList.toggle("dark", next === "dark");
try { localStorage.setItem("theme", next); } catch {}
setTheme(next);
}, []);
return { theme, setTheme: apply, toggle: () => apply(theme === "dark" ? "light" : "dark") };
}
```
Ship a **visible toggle** (a sun/moon button in the header or settings). If the app has a **command palette** (⌘K), also register "Toggle theme / Switch to dark mode" there — power users expect it.
## Step 3 — migrate raw utilities to tokens (per route)
This is the bulk, and it's the **parallelizable** part — the work partitions cleanly by file, so fan it out one agent per route behind a build barrier (see `parallel-agent-refactor`). Every migrating agent carries the **same two artifacts**: the mapping table below and the do-not-touch list. The rule for each agent: apply the mapping mechanically, leave anything on the do-not-touch list raw, and leave any shade with no exact token match **raw rather than guessing** (guessing breaks the pixel-identical-light invariant).
## The mapping table
The highest-value rows below (`stone` shown; same shape for `slate`/`gray`/`zinc`/`neutral`). **Full table — every shade row plus the do-not-touch list, formatted as the house-rules artifact to hand each fan-out agent: `templates/mapping-table.md`.**
| Raw utility | Token utility | Why |
|---|---|---|
| `bg-stone-50` | `bg-background` | the app canvas |
| `bg-white` | **`bg-card`** (NOT `bg-background`) | white panels/inputs must stay *lighter than* the canvas — in dark, card is stone-900 on a stone-950 page, preserving the white-on-paper contrast step |
| `text-stone-900/800/700` | `text-foreground` | primary text |
| `text-stone-600/500/400` | `text-muted-foreground` | secondary/label text |
| `border-stone-200/100` | `border-border` | dividers, card edges (form controls → `border-input`) |
| `bg-stone-900` **paired with** `text-stone-50` | `bg-primary` **+** `text-primary-foreground` | a dark emphasis box → tokenize the pair together |
Rule of thumb: **surfaces** map to `background`/`card`/`muted`/`accent`, **text** to `foreground`/`muted-foreground`, **lines** to `border`/`input`, **emphasis** to `primary`(+`-foreground`). `bg-white → bg-card` is the one everyone gets wrong — mapping it to `bg-background` collapses the contrast step and makes cards vanish into the page in dark.
## Do-not-touch list (leave these RAW)
These are intentionally not chrome, or have no exact token. Migrating them breaks the design or the invariant. Leave the raw class exactly as-is:
- **Deliberately-dark blocks** — a one-time-secret reveal, a code/terminal banner, a "copy this token" box built on `bg-stone-950`/`bg-stone-900` that is *meant* to be dark in **both** themes. It's not following the theme; it's a fixed dark surface.
- **Destructive / red** — the one accent hue (`bg-red-*`, `text-red-*`, `border-red-*`). It's already identical in both themes via `--destructive`; raw red usages stay raw.
- **Brand, data-viz, and provider colors** — logo colors, chart series palettes, a "Sign in with Google/GitHub" button's brand color. These are semantic to *their* meaning, not to the theme.
- **Customer-facing embedded previews** — a widget mock / iframe preview that always renders how the customer's end-users will see it (typically always light). It must not follow the operator's theme.
- **Overlay scrims** — `bg-stone-950/40` behind a modal should become **`bg-black/40`** (a fixed dark scrim), NOT a token. A scrim that inverts to light in dark mode stops dimming.
- **Any shade with no exact token** — an off-palette `stone-700` mid-tone, a `bg-stone-200` used somewhere the tokens don't cover. **Leave it raw rather than guessing** a near-match. A wrong guess is a visible light-mode regression; a raw class is at worst a spot that doesn't darken yet (catch it in visual QA).
## The paired-class trap + the cleanup grepFrom ever-just/agentskills, MIT.
More from ever-just/agentskills
- 0.575score
claude-session-archaeology
ever-just
Reconstruct what work was actually done on a project by mining Claude Code session transcripts (~/.claude/projects/**/*.jsonl) — streaming grep/jq recipes for…
SkillBFreshness: B★ 5Claude Code +9 - 0.529score
deploy-log-forensics
ever-just
Reconstruct what changed on production — which commit, which module, which tenant, at what time — from CI/CD run logs when you have NO server access (no SSH…
SkillCFreshness: C★ 5Claude Code +9 - 0.529score
agent-quality-grading
ever-just
Grade AI-agent performance qualitatively — every user↔agent conversation on four axes (did it do the task / respond fast / use the right tools / message…
SkillCFreshness: C★ 5Claude Code +9 - 0.439score
canvas-design
ever-just
Create beautiful visual art in .png and .pdf documents using design philosophy.
SkillDFreshness: D★ 5Claude Code +9 - 0.283score
client-discovery-osint
ever-just
No description from the publisher yet.
SkillCFreshness: C★ 5Claude Code +9 - 0.283score
ad-transparency-audit
ever-just
No description from the publisher yet.
SkillCFreshness: C★ 5Claude Code +9 - 0.283score
commercial-property-research
ever-just
No description from the publisher yet.
SkillCFreshness: C★ 5Claude Code +9 - 0.283score
agent-discoverability
ever-just
No description from the publisher yet.
SkillCFreshness: C★ 5Claude Code +9 - 0.283score
business-model-canvas
ever-just
No description from the publisher yet.
SkillCFreshness: C★ 5Claude Code +9 - 0.283score
cdp-render-verification
ever-just
No description from the publisher yet.
SkillCFreshness: C★ 5Claude Code +9 - 0.283score
company-legal-reputation-research
ever-just
No description from the publisher yet.
SkillCFreshness: C★ 5Claude Code +9 - 0.283score
competitor-identification
ever-just
No description from the publisher yet.
SkillCFreshness: C★ 5Claude Code +9
Signals
- Stars
- 5
- Forks
- 1
- Open issues
- 0
- Last commit (this folder)
- Jul 12, 2026 (3 months ago)
From GitHub (fetched Oct 24, 2026).
Details
- License
- MIT
- Repository
- github.com/ever-just/agentskills
- First seen
- Oct 24, 2026
- Last checked
- Oct 24, 2026
- Source
- GitHubgithub-graphql
More from ever-just
- 0.575score
claude-session-archaeology
ever-just
Reconstruct what work was actually done on a project by mining Claude Code session transcripts (~/.claude/projects/**/*.jsonl) — streaming grep/jq recipes for…
SkillBFreshness: B★ 5Claude Code +9 - 0.529score
deploy-log-forensics
ever-just
Reconstruct what changed on production — which commit, which module, which tenant, at what time — from CI/CD run logs when you have NO server access (no SSH…
SkillCFreshness: C★ 5Claude Code +9 - 0.529score
agent-quality-grading
ever-just
Grade AI-agent performance qualitatively — every user↔agent conversation on four axes (did it do the task / respond fast / use the right tools / message…
SkillCFreshness: C★ 5Claude Code +9 - 0.439score
canvas-design
ever-just
Create beautiful visual art in .png and .pdf documents using design philosophy.
SkillDFreshness: D★ 5Claude Code +9
Done for you
Rather not set it up yourself?
Chalk Labs scopes, builds and runs agents for your team: the task audit, the eval harness, the integrations and the weekly ops report. Fixed scope, priced before we start.
See how we build AI agents →Read the case studies
Chalk Labs is a two-founder AI and growth studio. We build AI agents, CRMs and MVPs for Web3 and AI companies, and have shipped 50+ product and token launches.
- launches shipped
- 50+
- launches shipped
- media placements
- 500+
- media placements
- founders on every account
- 2
- founders on every account
Press we've placed: Forbes · CoinDesk · Decrypt · Cointelegraph · The Block
Rahil Jain, Co-founder, Chalk Labs. Ex-Goldman Sachs engineer. You talk to the person who builds it.
Get dark-mode-token-migration running in your business
Tell us what you want to automate. A founder replies within one working day with a plan, not a pitch deck.
Cite this page
dark-mode-token-migration skill. Agent Mart. https://agentmart.dev/skills/ever-just-dark-mode-token-migration (last updated 2026-10-24)