Skip to content
Agentmart
Submit a tool

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

IndexedMIT★ 5
Write a review

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.

Book a call

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.

We use your email only for this newsletter. Privacy

About

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.

Use cases: design, frontend

No 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 grep

From ever-just/agentskills, MIT.

More from ever-just/agentskills

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
First seen
Oct 24, 2026
Last checked
Oct 24, 2026

More from ever-just

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.

Goes to the Chalk Labs founders, not the newsletter. Privacy

Need help deploying dark-mode-token-migration?

CallGet help
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)