Production CSS Styling with Claude Code: Layers, Tokens, and Visual Checks
Use Claude Code to harden production CSS with layers, tokens, container queries, dark mode, a11y, and visual checks.
CSS looks like an easy place to use Claude Code, but production styling is not only about making a screenshot look close. You need a CSS architecture, reusable tokens, responsive rules, dark mode, accessibility checks, and regression tests. This guide shows the workflow I use for a small pricing card so Claude Code can move fast without turning the stylesheet into a pile of one-off fixes.
In this article, design tokens mean named values for colors, spacing, radius, and shadows. Cascade layers are priority shelves for CSS rules. Container queries are conditions based on the parent container, not the whole viewport. Saying these plain definitions inside the prompt makes Claude Code much less likely to invent a parallel styling system.
Keep the official references open while adapting the examples: MDN @layer, CSS custom properties, container queries, prefers-color-scheme, W3C contrast guidance, and Playwright visual comparisons. For the Claude Code side, use Claude Code common workflows. For local team rules, pair this with CLAUDE.md best practices and testing strategies.
Build The Styling Harness First
If you ask Claude Code to “clean up the CSS,” it may produce a quick visual improvement and a long-term maintenance problem. Real projects often mix global CSS, CSS Modules, Tailwind utilities, markdown content styles, browser defaults, and legacy overrides. Without a boundary, the assistant may add stronger selectors instead of fixing the architecture.
Start with a harness, meaning the safe working frame for the agent. Give it the target files, the layers it may touch, the token naming rules, the commands it must run, and the areas it must not edit. Styling changes are visually rewarding, so they grow quickly. The harness keeps the diff reviewable.
flowchart LR
P["Claude Code prompt"] --> L["cascade layers"]
L --> T["design tokens"]
T --> C["card/button components"]
C --> R["responsive + container queries"]
R --> D["dark mode"]
D --> A["accessibility checks"]
A --> V["Playwright visual regression"]
Use an inspection prompt before allowing edits:
Read AGENTS.md, CLAUDE.md, package.json, and every file under src/styles.
Do not edit yet.
Report the current CSS architecture, naming conventions, token usage,
dark-mode strategy, responsive breakpoints, and test commands.
Then propose the smallest safe plan for a pricing card and CTA button.
This prompt prevents the most common failure: Claude Code skipping the existing system and creating a new one. A production site may have reset.css, global.css, component styles, and CMS content styles with different owners. The human decision is the boundary. The assistant’s job is to implement inside that boundary.
Lock Priority With Cascade Layers
The cascade decides which rule wins when several rules match the same element. Teams often work around conflicts with higher specificity, later imports, or !important. That may unblock one release, but it makes the next change harder. Cascade layers let you name the priority shelves explicitly.
The file below is a copy-pasteable baseline for a small experiment. It separates tokens, base, components, utilities, and overrides. Treat overrides as a temporary quarantine, not as a normal feature layer.
/* src/styles/app.css */
@layer reset, tokens, base, components, utilities, overrides;
@import "./tokens.css" layer(tokens);
@import "./base.css" layer(base);
@import "./components.css" layer(components);
@import "./utilities.css" layer(utilities);
@layer reset {
*,
*::before,
*::after {
box-sizing: border-box;
}
body,
h1,
h2,
h3,
p {
margin: 0;
}
}
@layer overrides {
.legacy-markdown :where(table, pre) {
max-width: 100%;
}
}
Ask Claude Code to classify existing CSS before rewriting it:
Move existing global CSS into the layer model in src/styles/app.css.
Do not change class names used by templates.
Use reset, tokens, base, components, utilities, and overrides only.
If a rule must go into overrides, explain why in the final response.
Run npm test and the visual check command after editing.
The pitfall is a half-migration. Regular unlayered rules can still outrank layered rules, and @import has placement rules of its own. Do not layer one random block and assume the cascade is now documented. Start with one new component, verify the behavior, and then migrate older CSS in small passes.
Store Design Decisions As CSS Tokens
Tokens are not decorative. They are how you stop Claude Code from inventing a new green, a new shadow, and a new button radius every time it touches a feature. In plain terms, a token is a named design decision stored as a variable.
/* src/styles/tokens.css */
@layer tokens {
:root {
color-scheme: light;
--color-bg: #f7f7f2;
--color-surface: #ffffff;
--color-text: #1f2933;
--color-muted: #5d6673;
--color-border: #d9ded7;
--color-accent: #0f766e;
--color-accent-strong: #0b4f49;
--color-focus: #b45309;
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 0.75rem;
--space-4: 1rem;
--space-6: 1.5rem;
--space-8: 2rem;
--radius-sm: 0.25rem;
--radius-md: 0.5rem;
--shadow-card: 0 0.75rem 2rem rgb(31 41 51 / 0.12);
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme="light"]) {
color-scheme: dark;
--color-bg: #111827;
--color-surface: #1f2937;
--color-text: #f9fafb;
--color-muted: #cbd5e1;
--color-border: #475569;
--color-accent: #2dd4bf;
--color-accent-strong: #99f6e4;
--color-focus: #fbbf24;
--shadow-card: 0 0.75rem 2rem rgb(0 0 0 / 0.32);
}
}
:root[data-theme="dark"] {
color-scheme: dark;
--color-bg: #111827;
--color-surface: #1f2937;
--color-text: #f9fafb;
--color-muted: #cbd5e1;
--color-border: #475569;
--color-accent: #2dd4bf;
--color-accent-strong: #99f6e4;
--color-focus: #fbbf24;
--shadow-card: 0 0.75rem 2rem rgb(0 0 0 / 0.32);
}
}
For production work, ask for contrast checks, not just color changes. W3C’s WCAG guidance expects normal text to reach at least 4.5:1 contrast. That means the assistant should review body text, muted text, links, buttons, and focus rings in both themes.
Keep Card And Button CSS In The Component Layer
Cards and buttons appear everywhere: blogs, SaaS dashboards, landing pages, settings screens, and checkout flows. If Claude Code adds generic .title and .button classes, another page will eventually break. Use names that reveal ownership.
/* src/styles/components.css */
@layer components {
.ui-card {
container: card / inline-size;
display: grid;
gap: var(--space-4);
padding: var(--space-6);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
background: var(--color-surface);
color: var(--color-text);
box-shadow: var(--shadow-card);
}
.ui-card__eyebrow {
color: var(--color-accent);
font-size: 0.875rem;
font-weight: 700;
}
.ui-card__title {
max-width: 18ch;
font-size: clamp(1.5rem, 1rem + 2cqi, 2.25rem);
line-height: 1.1;
}
.ui-card__body {
color: var(--color-muted);
font-size: 1rem;
line-height: 1.7;
}
.ui-actions {
display: flex;
flex-wrap: wrap;
gap: var(--space-3);
align-items: center;
}
.ui-button {
display: inline-flex;
min-height: 2.75rem;
align-items: center;
justify-content: center;
padding: 0.75rem 1rem;
border: 1px solid transparent;
border-radius: var(--radius-sm);
background: var(--color-accent);
color: var(--color-surface);
font: inherit;
font-weight: 700;
text-decoration: none;
}
.ui-button:hover {
background: var(--color-accent-strong);
}
.ui-button:focus-visible {
outline: 3px solid var(--color-focus);
outline-offset: 3px;
}
.ui-button[aria-disabled="true"],
.ui-button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
}
Create a stable preview route for verification. In Astro, Next.js, or Vite, this can live at /style-lab. The important part is that visual tests have deterministic content.
<main class="style-lab">
<article class="ui-card" data-testid="pricing-card">
<p class="ui-card__eyebrow">Team plan</p>
<h1 class="ui-card__title">Ship production CSS with Claude Code</h1>
<p class="ui-card__body">
Layer your CSS, reuse tokens, check dark mode, and catch visual regressions before release.
</p>
<div class="ui-actions">
<a class="ui-button" href="/training/">Start with training</a>
<a class="ui-button" href="/thanks/">Get the free checklist</a>
</div>
</article>
</main>
Use data-testid for visual targets and roles or labels for interaction checks. Visual tests should survive copy changes, while accessibility tests should use the names a real user hears or sees.
Separate Viewport Rules From Container Rules
Media queries respond to the viewport. Container queries respond to the space available to a component. That difference matters when the same card appears in a full-width landing page, a narrow sidebar, and a dashboard grid.
/* src/styles/responsive.css */
@layer components {
.style-lab {
display: grid;
min-height: 100svh;
place-items: center;
padding: clamp(1rem, 4vw, 4rem);
background: var(--color-bg);
}
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
gap: var(--space-6);
width: min(100%, 72rem);
}
@container card (min-width: 36rem) {
.ui-card {
grid-template-columns: 1fr auto;
align-items: center;
}
.ui-card__body {
max-width: 58ch;
}
.ui-actions {
justify-content: end;
}
}
@media (max-width: 40rem) {
.ui-card {
padding: var(--space-4);
}
.ui-actions,
.ui-button {
width: 100%;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
transition-duration: 0.001ms !important;
animation-duration: 0.001ms !important;
animation-iteration-count: 1 !important;
}
}
}
Do not prompt with “make it responsive.” Give concrete widths: 320, 375, 768, 1024, and 1440 pixels. Ask Claude Code to check horizontal overflow, text wrapping, focus-ring clipping, button height, and dark-mode layout at each width.
Practical Use Cases
Use case one is a monetization card on a blog or landing page. Ads, affiliate blocks, consulting CTAs, and free-download CTAs must be visible without causing layout shift or visual clutter. Ask Claude Code to ship the component CSS, responsive behavior, dark mode, and visual checks in one small change.
Use case two is a SaaS settings screen. Cards, forms, destructive actions, and saved states often share the same surface. If every page invents its own colors, future brand changes become expensive. Tell Claude Code to use existing tokens only and stop for review if a new token is needed.
Use case three is legacy CMS content. Markdown and MDX bodies often have global rules for h2, table, pre, and blockquote. A bulk rewrite can break hundreds of posts. Keep the changes under a wrapper such as .legacy-markdown and compare a few published pages before shipping.
Use case four is the first step toward a design system. Do not replace every component at once. Start with common pieces such as cards and buttons. The smaller the diff, the easier it is for Claude Code to follow the existing project style and for reviewers to trust the change.
Pitfalls To Catch In Review
The first pitfall is !important. It can hide a problem for one release and create a stronger conflict later. Tell Claude Code not to add !important; if it believes one is necessary, it should explain the selector conflict instead.
The second pitfall is dark mode without contrast checks. Changing the background is not enough. Body text, muted text, links, buttons, borders, and focus rings all need to work together.
The third pitfall is using viewport width for everything. A card inside a sidebar can be narrow on a wide desktop screen. Container queries solve that class of problem better than another breakpoint.
The fourth pitfall is flaky visual testing. External fonts, animations, random data, and live ads can make screenshots fail for the wrong reason. Use stable fixtures, reduced motion, and controlled content before comparing pixels.
Add Playwright Visual Regression
CSS should not rely only on human eyeballing. Playwright screenshots let you keep checking the card at several widths, in light mode, in dark mode, and with keyboard focus visible.
// tests/visual/style-regression.spec.ts
import { expect, test } from "@playwright/test";
const viewports = [
{ name: "mobile", width: 375, height: 812 },
{ name: "tablet", width: 768, height: 1024 },
{ name: "desktop", width: 1440, height: 900 },
];
for (const viewport of viewports) {
test(`pricing card visual regression ${viewport.name}`, async ({ page }) => {
await page.setViewportSize({ width: viewport.width, height: viewport.height });
await page.emulateMedia({ colorScheme: "light", reducedMotion: "reduce" });
await page.goto("/style-lab");
const card = page.getByTestId("pricing-card").first();
await expect(card).toBeVisible();
await expect(card).toHaveScreenshot(`pricing-card-${viewport.name}-light.png`, {
animations: "disabled",
maxDiffPixelRatio: 0.02,
});
});
}
test("dark theme keeps focus states visible", async ({ page }) => {
await page.emulateMedia({ colorScheme: "dark", reducedMotion: "reduce" });
await page.goto("/style-lab");
await page.locator("html").evaluate((element) => {
element.setAttribute("data-theme", "dark");
});
const startButton = page.getByRole("link", { name: /start with training/i });
await startButton.focus();
await expect(startButton).toBeFocused();
await expect(page.getByTestId("pricing-card").first()).toHaveScreenshot(
"pricing-card-dark-focus.png",
{
animations: "disabled",
maxDiffPixelRatio: 0.02,
},
);
});
Create snapshots once, then run the check normally:
npx playwright test tests/visual/style-regression.spec.ts --update-snapshots
npx playwright test tests/visual/style-regression.spec.ts
Finish with a fixed review prompt:
Critically review the CSS diff.
Check cascade layers, token usage, selector specificity, dark mode,
container queries, keyboard focus, color contrast, reduced motion,
and Playwright visual coverage.
Return only concrete issues with file paths and line numbers.
Monetization CTA And Field Notes
A CSS article should not end at the code sample. Individual builders can use the free checklist to review their next styling change. Teams evaluating a broader rollout can move to Claude Code training and consultation. For repeatable operating rules, read harness engineering.
When I tested this workflow, the biggest improvement came from the inspection prompt, not from @layer alone. If Claude Code first read the existing styles, tokens, and verification commands, its diff stayed focused on the card and button. If I asked it to “polish the styling,” it added hard-coded colors, duplicate spacing, and no visual checks. Claude Code can write CSS quickly, but production quality comes from the human-owned architecture, token names, and regression commands that frame the work.
Free PDF: Claude Code Cheatsheet
Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.
We handle your data with care and never send spam.
Level up your Claude Code workflow
Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.
About the Author
Masa
Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.
Related Posts
Claude Code Permission Safety Ladder: Expand Access Without Losing Control
A beginner-friendly ladder for moving Claude Code from read-only to limited edits, proof commands, and deploy checks.
Claude Code Small PR Proof Pack: Make Tiny Changes Reviewable
A practical proof pack for Claude Code PRs: diff, checks, public URL, CTA path, and rollback note.
Claude Code Review Gate Before Commit: Diff, Tests, Public URL, and CTA Checks
A commit-time review gate for Claude Code work: diff scope, build, public URL, revenue CTA links, missing tests, and unrelated files.
Related Products
50 Battle-Tested Claude Code Prompt Templates
Copy, paste, ship. 50 production-ready prompts.
Use proven prompts for code review, refactoring, testing, documentation, debugging, architecture, and incident response.
The Complete Claude Code Setup & Configuration Guide
From install to team-ready workflow.
A practical guide to installation, CLAUDE.md, hooks, MCP servers, permissions, IDE setup, and CI/CD workflows.