Themable HTML, without the boilerplate. Write semantic classes once — the browser, the user, or a Chrome extension can repaint everything.
Once Claude artifacts moved from a 30-line markdown file to a 200-line HTML one, two things happened. The output got dramatically richer — diagrams, tables, embedded interactions. But every artifact also started baking its own colors, its own dark-mode toggle, its own copy of "what looks good".
Chameleon is the smallest amount of structure that lets one Skill stop reinventing the palette, one Chrome extension override every artifact's theme without touching the file, and a future you read your own HTMLs in your own colors — three months later, on a different laptop, in a different mood.
The Why isn't "dark mode is nice". It's that the artifact and the viewer are different people now — the model writes the structure, you bring the taste.
Chameleon is the agreement between a CSS file, a Skill, and a Chrome extension. Each is independently useful, but together they let any HTML behave like a Tailwind-themed site without ever importing Tailwind.
| Component | Owns | Output |
|---|---|---|
| theme/v1/* | The contract: CSS variables & utility class names | Two static files served from GitHub Pages |
| /skill | Claude-side authoring: emit / refactor HTML to use the contract | Generated or converted .html files |
| /extension | User-side viewing: settings UI & runtime override | localStorage entry consumed by theme.js |
Themes never address pixels — only roles. --primary is "the color you click",
not "blue". This is the entire contract a theme must implement; everything else is built from these.
Why these and not more? Every variable a theme has to define is a tax on contributors. 21 covers 95% of artifacts. Anything else (chart palettes, syntax-highlight colors) goes in a v2 namespace and stays optional.
The class names are intentionally close to Tailwind's mental model — but every value resolves to a CSS variable, not a hardcoded hue. Compose them just like you would in Tailwind.
| Class | Resolves to | Demo |
|---|---|---|
| bg-canvas | background: var(--canvas) |
Canvas |
| bg-surface | background: var(--surface) |
Surface |
| bg-surface-2 | background: var(--surface-2) |
Elevated |
| bg-primary | bg + auto on-primary text | Primary |
| bg-secondary | bg + auto on-secondary text | Secondary |
| bg-accent | bg + auto on-accent text | Accent |
| bg-success / warning / danger | state bg + matching text | OK ! × |
| text-base / muted / subtle | three text shades | Aa Aa Aa |
| text-primary / secondary / accent | brand-colored text | Aa Aa Aa |
| border / border-subtle / border-strong | three border weights | · · · |
| rounded / rounded-lg / rounded-full | radius helpers | ▢ ▢ ● |
Yes. Chameleon classes are intentionally narrow in scope (color & border only). Spacing, layout, typography sizes — keep using Tailwind, raw CSS, or whatever you prefer. Chameleon stays out of those decisions.
The picker on the left writes inline CSS variables on :root — exactly what the Chrome extension will do.
The preview, this page, and every Chameleon-themed artifact share the same variables.
Welcome flow A/B test concluded. Variant B (with progress bar) outperformed control on day-3 retention. Recommend rolling out to 100% in the next release.
// theme.js — load from localStorage, fall back to system pref const stored = localStorage.getItem('chameleon-theme'); const theme = stored ? JSON.parse(stored) : { mode: 'light' }; document.documentElement.dataset.theme = theme.mode;
Most of the value is in retrofit — the dozens of HTML artifacts you already have.
The Skill scans inline styles, <style> blocks, and SVG fill attrs,
proposes a role mapping, and shows a dry-run before touching anything.
<div style="background:#1a1a2e;
color:#e0e0e0;
border:1px solid #16213e">
<h2 style="color:#0f3460">Report</h2>
<button style="background:#e94560;
color:white">
Run
</button>
</div>
<div class="bg-surface text-base
border rounded">
<h2 class="text-primary">Report</h2>
<button class="btn btn-primary">
Run
</button>
</div>
// + <link href="...theme.css">
// + <script src="...theme.js">
| Original color | Proposed role | Confidence | Action |
|---|---|---|---|
| #1a1a2e | --surface | high · 92% | auto |
| #e94560 | --primary | high · 88% | auto |
| #0f3460 | --secondary | medium · 64% | confirm |
| #16213e | --border | medium · 58% | confirm |
| SVG fill (illustration) | — preserve — | opt-out | skip |
Anything below 70% confidence pauses for your sign-off. Illustration / brand-locked colors get an explicit opt-out marker so they survive future passes.
Everything ships from one repo. The contract files are versioned under /v1 so a breaking change becomes /v2 rather than a silent regression.
html-chameleon/ ├── theme/ │ └── v1/ │ ├── theme.css 23 CSS variables + utility classes │ └── theme.js localStorage loader, system-pref fallback ├── skill/ │ ├── SKILL.md Claude Code skill — generate / convert modes │ └── prompts/ │ ├── generate.md │ └── convert.md ├── extension/ │ ├── manifest.json Chrome MV3 │ ├── popup.html settings panel UI │ ├── content.js injects floating button + sets localStorage │ └── background.js cross-tab sync ├── examples/ │ ├── basic.html "hello world" themed page │ ├── dashboard.html complex layout demo │ └── conversion.html before/after sample ├── docs/ │ └── design.html ↩ this file ├── README.md └── LICENSE MIT
GitHub Pages serves /theme/v1/theme.css at a stable URL. Themed HTML files reference it with one
<link> tag. The contract is small enough that anyone reading the README can author a new preset in 5 minutes.
A single Skill with two modes. It does not live in CLAUDE.md — it auto-triggers only when the request is HTML-related, keeping the regular agent context lean.
| Mode | Triggered by | Behavior |
|---|---|---|
| generate | "make an HTML…", "create an artifact…" | Emit new HTML with the standard <link>/<script> bootstrap and Chameleon classes throughout. |
| convert | "make this themable", "Chameleon-ify" | Read existing HTML → propose color→role mapping → dry-run preview → write on approval. |
<link rel="stylesheet" href="https://churin1116.github.io/html-chameleon/theme/v1/theme.css"> <script src="https://churin1116.github.io/html-chameleon/theme/v1/theme.js"></script> <html data-theme="light"> <!-- default; overridden by extension/localStorage -->
data-chameleon="ignore" on an element stay untouched on every future pass.file.original.html alongside any in-place conversion.
The extension is purely additive: HTMLs work without it (they get the default theme baked into theme.css),
they just don't get the live picker. This keeps shareability intact.
<link> referencing html-chameleon and shows the floating dial.localStorage['chameleon-theme']; theme.js already listens for changes.chrome.storage.sync.
Before the v1 contract gets locked at /v1/theme.css, these are the calls worth making explicitly.
Should we prefix utilities (cm-bg-primary) for safety, or stay clean and document the collision risk?
Right now Chameleon owns color only. Shadows are surprisingly theme-dependent in dark mode. Worth 3 more vars?
If a page doesn't reference Chameleon, should the extension offer to inject the theme.css link client-side as a "force theme" mode?
Pure-prompt convert is portable; a small script could parse stylesheets more reliably. Maybe both, with prompt as fallback.
GitHub Pages has no edge cache headers control. For a globally-loaded stylesheet, jsDelivr or a tiny Cloudflare worker may be worth it later.
For non-Claude users embedding theme.css in build pipelines, an html-chameleon npm package costs nothing extra and widens reach.