Claude Code for Frontend Development: A Practical Workflow
Why Frontend Work Feels Different With an Agent in the Loop
Frontend development has always been a strange mix of precise engineering and constant visual judgment calls. You're not just writing functions that either pass a test or don't — you're deciding whether a card component "feels" cramped, whether a hover state is too aggressive, whether a grid should collapse to two columns or one at 768px. This is exactly the kind of work that used to resist AI assistance. Autocomplete tools could finish a line of JSX, but they had no opinion about your design system, no memory of the component three files away that already solved this problem, and no way to actually run the app and look at the result.
Claude Code changes the equation because it operates as an agent inside your terminal, not a text predictor inside your editor. It can read your entire component tree, understand how your Tailwind config or CSS variables are structured, run your dev server, and — critically — look at what actually rendered. That last part matters more than people expect. A huge share of frontend bugs are only obvious once you see them: a button that's technically centered but visually off because of a shadow, a modal that clips on mobile, text that overflows a fixed-height card. An agent that can only read code will confidently tell you the CSS is correct while the layout is broken.
This article walks through a concrete, repeatable workflow for using Claude Code on real frontend tasks — building a new component from a spec, refactoring an existing one without breaking consumers, and debugging a CSS layout bug that doesn't show up until you actually render the page. None of this is theoretical. It's the loop that tends to work when you use Claude Code daily on production React and CSS: describe intent clearly, let the agent write and run code, and treat the rendered output as the actual source of truth, not the code diff.
Setting Up the Frontend Feedback Loop
Before touching a single component, the workflow that pays off is making sure Claude Code can see what the browser sees. Terminal-only agents guess at layout; agents wired to a live preview verify it. In practice this means starting your dev server and keeping a preview loop active for the whole session, not just at the end when you want a screenshot.
A typical setup for a Vite or Next.js project looks like this:
# Start the dev server the same way you normally would
npm run dev
# Claude Code can also launch and manage this for you,
# reusing the same server across a session instead of
# restarting it for every checkOnce the server is running, the useful pattern is: make a change, render it, inspect specific elements, only then move to the next change. Inspecting is different from screenshotting. A screenshot tells you roughly what something looks like; inspecting a specific selector tells you the exact computed padding, color, and box dimensions, which is what actually determines whether a design spec was met. Screenshots are for catching things you didn't think to check — an inspect call is for confirming the thing you did check.
The other half of the loop is the browser console and network tab. Frontend bugs are frequently invisible in the DOM but loud in the console — a failed fetch, a React key warning, a hydration mismatch. Treating console errors as part of "does this work" rather than a separate debugging step later saves a lot of round trips. The habit worth building is: after any non-trivial change, check render, check console, check one or two interactive states (hover, focus, error), and only then consider the task done.
Building a Component From a Spec: A Card Component Walkthrough
Say the task is a PricingCard component: a title, a price, a feature list, a CTA button, and a "highlighted" variant for the recommended plan. This is a good example task because it has real design constraints (spacing, emphasis, responsive behavior) but is small enough to walk through completely.
The first move is not to ask for the whole component blind. Frontend tasks go better when you give the agent the actual constraints up front — existing design tokens, spacing scale, and how similar components in the codebase are structured. If your project has a tailwind.config.js or a theme.css with custom properties, pointing at those first means the generated component matches the rest of the app instead of inventing its own scale.
Here's a reasonable starting component, written to use CSS custom properties rather than hardcoded values, so it inherits the app's theme automatically:
// PricingCard.jsx
import { useState } from "react";
export function PricingCard({
title,
price,
period = "/month",
features = [],
ctaLabel = "Get started",
onSelect,
highlighted = false,
}) {
const [isPressed, setIsPressed] = useState(false);
return (
<div
className={`pricing-card ${highlighted ? "pricing-card--highlighted" : ""}`}
role="group"
aria-label={`${title} plan`}
>
{highlighted && <span className="pricing-card__badge">Most popular</span>}
<h3 className="pricing-card__title">{title}</h3>
<p className="pricing-card__price">
<span className="pricing-card__amount">{price}</span>
<span className="pricing-card__period">{period}</span>
</p>
<ul className="pricing-card__features">
{features.map((feature) => (
<li key={feature}>{feature}</li>
))}
</ul>
<button
type="button"
className="pricing-card__cta"
onMouseDown={() => setIsPressed(true)}
onMouseUp={() => setIsPressed(false)}
onClick={() => onSelect?.(title)}
data-pressed={isPressed}
>
{ctaLabel}
</button>
</div>
);
}And the matching CSS, deliberately using variables instead of literals so a theme change upstream doesn't require touching this file again:
/* pricing-card.css */
.pricing-card {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-3, 0.75rem);
padding: var(--space-6, 1.5rem);
border: 1px solid var(--color-border, #e2e2e2);
border-radius: var(--radius-lg, 12px);
background: var(--color-surface, #fff);
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.pricing-card--highlighted {
border-color: var(--color-accent, #6b4eff);
box-shadow: 0 8px 24px rgba(107, 78, 255, 0.15);
transform: translateY(-4px);
}
.pricing-card__badge {
position: absolute;
top: -0.75rem;
left: var(--space-6, 1.5rem);
padding: 0.25rem 0.75rem;
font-size: 0.75rem;
font-weight: 600;
color: #fff;
background: var(--color-accent, #6b4eff);
border-radius: 999px;
}
.pricing-card__cta {
margin-top: auto;
padding: var(--space-3, 0.75rem) var(--space-4, 1rem);
border-radius: var(--radius-md, 8px);
border: none;
font-weight: 600;
cursor: pointer;
transition: transform 0.1s ease;
}
.pricing-card__cta[data-pressed="true"] {
transform: scale(0.97);
}Notice this component makes several judgment calls that a spec rarely states explicitly: margin-top: auto on the CTA so buttons align across a row of cards even when feature lists differ in length, a data-pressed attribute instead of a CSS-only :active pseudo-class so pressed state persists correctly on touch devices, and fallback values in every var() call so the component degrades reasonably even if a token is missing. These are the details worth explicitly asking for, because a generic request like "build me a pricing card" tends to produce something structurally fine but missing this kind of polish.
The Inspect-First Debugging Pattern
CSS bugs are the category where "just read the code" fails hardest, because CSS is contextual — a rule's effect depends on specificity, cascade order, parent flex/grid context, and box model interactions that aren't visible by reading one file in isolation. The workflow that actually resolves these reliably is: render the page, inspect the broken element's computed styles, form a hypothesis, make one change, re-inspect.
Take a common real bug: a card grid where cards in the same row have different heights because one has a longer description, and the CTA buttons end up at different vertical positions.
/* Before: cards are just block children, heights vary naturally */
.pricing-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: var(--space-6, 1.5rem);
}Reading this CSS alone looks correct — grid items in CSS Grid stretch to fill the row height by default, so the fix people usually reach for first (align-items: stretch) is already the default and won't help. The actual cause is almost always inside the card: if .pricing-card isn't a flex column with height: 100%, it will size to its content instead of its grid cell, and the CTA won't align across siblings even though the grid row itself is a uniform height.
/* Fix: make the card fill its grid cell, then push the CTA to the bottom */
.pricing-card {
display: flex;
flex-direction: column;
height: 100%; /* fill the grid row, not just content height */
}
.pricing-card__cta {
margin-top: auto; /* pushes to bottom regardless of content above it */
}The reason this workflow matters: if you only asked the agent to "read the CSS and find the bug," a plausible-sounding but wrong diagnosis (like blaming align-items) is easy to produce, because it matches common StackOverflow answers for grid-height problems even when it doesn't apply here. Confirming against the actual rendered box model — inspecting the card's height and the grid cell's height side by side — is what tells you definitively whether the mismatch is in the grid or inside the card. This is the single highest-leverage habit in the whole workflow: prefer "let's look at what rendered" over "let's reason about what should render."
Refactoring Without Breaking Every Caller
Component refactors are where frontend work quietly turns risky. A Button or Modal component used in forty places across a codebase can't just be rewritten — every call site's props, event handlers, and assumptions about DOM structure need to keep working, or you get a wave of small breakages that surface days later as visual regressions nobody connects back to the refactor.
The workflow that holds up here is treating the refactor as two separate passes rather than one big rewrite. First, a search pass: find every place the component is imported and used, and actually read enough of each call site to understand what props are passed and what markup structure callers might be relying on (some codebases have CSS that targets a component's internal DOM via descendant selectors, which is exactly the kind of hidden dependency that breaks silently). Second, the rewrite pass, done with the constraint list from the first pass in hand rather than rewriting from scratch and hoping it's still compatible.
// Before refactor: Button.jsx handles three visually different
// variants with string-matching logic that's grown hard to extend
export function Button({ variant, size, children, ...props }) {
let className = "btn";
if (variant === "primary") className += " btn-primary";
if (variant === "secondary") className += " btn-secondary";
if (variant === "danger") className += " btn-danger";
if (size === "small") className += " btn-sm";
return (
<button className={className} {...props}>
{children}
</button>
);
}// After refactor: same public API (variant, size, children, ...props
// all still work identically), but internals use a lookup table
// instead of string-matching, and unknown variants no longer
// silently fall through with no styling at all
const VARIANT_CLASSES = {
primary: "btn-primary",
secondary: "btn-secondary",
danger: "btn-danger",
};
const SIZE_CLASSES = {
small: "btn-sm",
medium: "",
large: "btn-lg",
};
export function Button({ variant = "primary", size = "medium", children, className: extraClassName, ...props }) {
const classes = ["btn", VARIANT_CLASSES[variant] ?? VARIANT_CLASSES.primary, SIZE_CLASSES[size] ?? "", extraClassName]
.filter(Boolean)
.join(" ");
return (
<button className={classes} {...props}>
{children}
</button>
);
}The important detail is what didn't change: the prop names, the default rendering as a native <button>, and the fact that arbitrary ...props (like onClick, disabled, aria-label) still pass through untouched. The refactor also quietly fixes a real bug — the old version had no className passthrough, so any call site that tried to add a one-off class for a specific instance was silently ignored. Catching that kind of thing is exactly why the search-first pass matters: it's not just about not breaking things, it's about noticing what the current version already gets wrong for existing callers.
After a refactor like this, the verification step is non-negotiable: render every distinct variant and size combination that actually exists in the codebase, not just the ones you remember, and check them against the pre-refactor screenshots if you have them. "The code compiles and the props still match" is necessary but not sufficient — the only real confirmation is seeing each rendered variant look the same as before, or intentionally different in a way you meant.
Handling Responsive Behavior Without Guesswork
Responsive design is another area where reading code and seeing the result diverge often enough to matter. A media query can be syntactically correct and still produce a broken layout at some width nobody tested, because the interaction between a flex container, its children's min-width, and text wrapping is genuinely hard to predict from source alone.
The practical workflow is to treat three widths as a minimum check for any layout change: a narrow phone width (roughly 375px), a tablet width (roughly 768px), and a standard desktop width (roughly 1280px). Resizing the actual preview to each of these and inspecting the result catches the two most common responsive bugs: content that doesn't reflow at all (fixed widths that should have been flexible), and content that reflows but overflows its container (usually a missing min-width: 0 on a flex child, since flex items default to min-width: auto, which prevents them from shrinking below their content size).
/* Common trap: this flex child won't shrink no matter how narrow
the viewport gets, because flex items default to min-width: auto */
.card__description {
flex: 1;
}
/* Fix: explicitly allow the item to shrink below its content size */
.card__description {
flex: 1;
min-width: 0;
}This single-line fix resolves a disproportionate number of "text is overflowing its card on mobile" bugs, and it's a good example of why inspecting computed styles at each breakpoint beats reading the stylesheet: flex: 1 looks completely reasonable in isolation, and the actual constraint (min-width: auto being the flex default) isn't visible anywhere in the CSS you wrote — it's a browser default that only shows up once you measure the rendered box at a narrow width.
Working With Design Systems and Component Libraries
Most real frontend work today happens inside an existing design system, not on a green field, which changes what "correct" means. A component that looks fine on its own can still be wrong if it introduces a new spacing value that isn't on the scale, a color that isn't a design token, or a shadow that doesn't match the elevation system used everywhere else.
The workflow adjustment here is front-loading context: before generating any new component, point at the existing token file, one or two similar existing components, and any documented spacing or type scale. This turns "build a card" into "build a card that reuses --space-4, --radius-md, and the existing Card component's shadow token" — a much more constrained and more likely to be correct request. It also makes review faster, because the diff is checking conformance to a known scale rather than evaluating a new set of arbitrary values on their own merits.
A related habit worth building: when a design system has a components directory, grep it for prior art before writing something from scratch. Frontend codebases accumulate a surprising amount of near-duplicate logic — three slightly different modal implementations, two different debounced-search-input patterns — and each new component built without checking makes the eventual consolidation harder. Five minutes of searching for "does something like this already exist" is consistently worth more than it costs.
A Realistic Session, End to End
Putting the pieces together, a typical frontend session with this workflow looks like: start the dev server and keep the preview loop open for the whole session. State the task with real constraints — existing tokens, similar components, specific prop requirements — rather than a bare description. Let the agent write the component and immediately render it rather than reading the JSX and moving on. Inspect the specific elements that matter for the spec (spacing, alignment, color) rather than eyeballing a screenshot. Resize to the three standard breakpoints and inspect again. Check the console for warnings or errors, especially React key warnings and hydration mismatches, which are easy to miss visually but indicate real bugs. Only then consider the component done.
For a refactor, the same shape applies with one extra step at the front: search for every call site before touching the component, and note anything unusual — a caller passing an undocumented prop, CSS that reaches into the component's internals via a descendant selector, a usage that relies on a bug in the current implementation. Skipping this step is the single most common cause of refactors that "work" in isolation but break something three files away.
None of this requires exotic tooling. It requires treating the rendered page as the actual specification and the source code as just one artifact that produces it — which, for frontend work specifically, is the mental shift that makes an AI coding agent genuinely useful instead of just fast at typing.
Where to Go From Here
This workflow — spec-first component generation, inspect-before-you-conclude debugging, search-then-rewrite refactoring, and breakpoint-by-breakpoint responsive checks — scales from a single pricing card up to a full design system migration. The core discipline stays the same at every size: state real constraints, render before judging, and verify against the actual browser rather than the code you think you wrote.
If you want a structured, guided path through this exact workflow — including live coding sessions on React components, CSS debugging drills, and refactor exercises on a real codebase — check out the Claude Code Tutorial for Beginners course on TeachYou.ai. It walks through everything covered here in depth, with hands-on projects designed to build the habits that make agentic frontend development reliable rather than lucky.
AI CodingShip full-stack AI apps at conversation speed — specs, agents, deploys, all from the terminal.
Claude CodeGo from zero to confident with Claude Code, the terminal agent that reads, edits, runs, and verifies real code.
Related reading