DEV Community

kent-tokyo
kent-tokyo

Posted on

What I Do Before Letting Claude Code Touch Web App Design

Claude Code writes code well, but "make it look nice" as a standalone instruction produces inconsistent results. Anthropic's best practices guide recommends separating exploration and planning from implementation, and using screenshots as verification signals rather than eyeballing the code. I extended that principle to the entire design process. Here is the sequence I use.


Before writing any code

Starting with code means design changes become code changes. If you decide later that you want a different layout, you end up dismantling components that are already written.

I do two things before touching code.

Get the spec in Markdown

Write a list of all screens in this app and the UI elements each screen needs.
No code yet.
Enter fullscreen mode Exit fullscreen mode

Claude Code writes out the screen inventory, per-screen elements, and data flow in prose. Deciding the layout direction here — sidebar or not, fixed header or not — reduces "actually, let's change this" iterations later.

Get the component tree as text

Based on the screen layout, write the parent-child relationships between components as a tree.
Include the props each component receives and the state it holds.
Enter fullscreen mode Exit fullscreen mode

Keep the output as text, not code. Reviewing the structure before implementation makes it easier to trace later why things ended up the way they did.


Build the design system first

Before creating a single component, lock in the rules for color, typography, and spacing.

Create the base of a design system with these constraints:
- Framework: Tailwind CSS
- Color palette: primary in blue, include grayscale
- Font: system font
- Define as CSS custom properties

No component code yet.
Create only tailwind.config.ts and globals.css.
Enter fullscreen mode Exit fullscreen mode

Skipping this leads to hardcoded color values scattered across components. Unifying text-blue-500 to text-primary everywhere afterward is tedious.

Without Tailwind, CSS custom properties work as a substitute:

:root {
  --color-primary: #2563eb;
  --color-surface: #f8fafc;
  --spacing-base: 0.25rem;
}
Enter fullscreen mode Exit fullscreen mode

Once the design direction is settled, I put it in DESIGN.md.

DESIGN.md follows a format open-sourced by Google Stitch, readable by Claude Code, Cursor, Copilot, and other agents. The structure has two layers:

---
colors:
  primary: "#2563eb"
  surface: "#f8fafc"
typography:
  base: "16px"
  scale: 1.25
spacing:
  unit: "4px"
components:
  library: "shadcn/ui"
---

## Design rationale

Primary color is blue for trust. Colors are chosen to meet WCAG AA contrast ratio (4.5:1 minimum for text on background).
Responsive is mobile-first, using only the `md` breakpoint.
Reference design: Stripe dashboard (high information density, minimal whitespace).
Enter fullscreen mode Exit fullscreen mode

The YAML frontmatter holds machine-readable tokens (color, font, spacing). The Markdown body holds the rationale — the "why" behind each value. Agents fill in edge cases from the rationale when tokens alone are not enough.

Add @DESIGN.md to CLAUDE.md to have Claude Code reference the design direction across sessions. No need to repeat it in every prompt.


Prompts for creating components

Design instructions land better when they say "what to reference" rather than "how it should look."

Less effective

Make it modern and clean.
Enter fullscreen mode Exit fullscreen mode

Claude Code's "modern" and your "modern" are different things.

More effective

Build this in a style close to shadcn/ui's Card component.
Use rounded-lg for border radius, shadow-sm for shadow, p-6 as the base padding.
Enter fullscreen mode Exit fullscreen mode
Design like the Stripe dashboard — high information density,
minimal whitespace, small font sizes.
Enter fullscreen mode Exit fullscreen mode

Naming a reference directly reduces variation in the output. Using shadcn/ui or Radix UI component names directly works for the same reason.

Specify interaction states from the start

Specifying only the default state leaves hover, disabled, and error states undefined. Listing all states up front avoids "the disabled state doesn't look right" fixes later.

Create a Button component. Implement all of these states:
- default: primary color background, white text
- hover: slightly darker (darken 10%)
- disabled: grayed out, cursor-not-allowed
- loading: replace text with a spinner, non-clickable
Enter fullscreen mode Exit fullscreen mode

For form input fields, specify the error state (red border, error message visible) at the same time.

Dark mode

Decide at the start whether to build dark mode in from the beginning or handle it later. Asking for dark mode after the fact means rewriting all the hardcoded light-mode values.

Add dark mode support using Tailwind's dark: classes.
The CSS variables in DESIGN.md already have both light and dark definitions. Reference those.
Enter fullscreen mode Exit fullscreen mode

Feedback loop

Once a component is working, I give feedback based on screenshots rather than reading through the code. The official best practices guide covers this pattern too: compare a screenshot against the target design and iterate from there.

I use /run to start the server, then use a screenshot tool (the frontend-design skill or Playwright MCP) to capture the screen before giving feedback.

Take a screenshot of the current home screen.
The header is too tall. Set it to h-16.
Bring the navigation font size down to text-sm.
Enter fullscreen mode Exit fullscreen mode

Packing too many changes into one instruction makes it hard to check what changed. I aim for two or three visually verifiable changes per round.

Responsive checks follow the same flow.

Take a screenshot at mobile width (375px).
The cards are probably still displaying side by side.
Fix them to stack vertically on mobile.
Enter fullscreen mode Exit fullscreen mode

When regressions happen

After enough iterations, "fixing one thing broke another" will come up. Asking Claude Code to compare the previous screenshot to the current state helps narrow down the cause.

Compare the previous screenshot to the current screen.
List any changes that happened unintentionally. No fixes yet.
Enter fullscreen mode Exit fullscreen mode

Understanding the cause before applying a fix avoids repeating the same patch cycle.


Things that kept going wrong for me

The drift problem bit me the most: "change the header font" would come back with the entire layout reorganized. Now I add an explicit boundary to any scoped change.

Change only the font size in the Header component.
Do not touch the layout or colors.
Enter fullscreen mode Exit fullscreen mode

I also tried generating all components in one pass a few times. It produced a lot of code quickly, but I'd invariably find structural issues only after everything was already built on top of that foundation. Building one component, confirming it, then moving on turned out to be faster overall even though it felt slower.

Vague feedback is the other thing that stalls progress. "The overall balance is wrong" is not a prompt Claude Code can act on. Taking a screenshot and naming specifics — "the left column is too wide relative to the right," "the font sizes look inconsistent between cards" — gets an actionable result.


The full sequence looks like this:

1. Get the spec in Markdown
2. Get the component structure as text
3. Build the design system (tailwind.config / globals.css)
4. Implement components one at a time
5. Review screenshots and iterate
Enter fullscreen mode Exit fullscreen mode

Skipping steps 1 to 3 and jumping into implementation consistently led to more rework later. Getting the structure right before writing code produced fewer revisions, at least in my experience.

Top comments (0)