How I build

·14 min read

I've spent the past year developing a process for building products with AI. Not a loose set of habits, but a repeatable sequence that I follow on every new project. It keeps evolving, but the bones are solid enough now that I think it's worth writing down.

The short version of my approach: make the important decisions early, then get into code as fast as possible and let the building teach you the rest.

A few principles sit underneath everything that follows.

The purpose of the design phase is to resolve ambiguity and make decisions, not to produce screens. And increasingly, the place those decisions live is not a design tool. It's the codebase. Your token scales, your component APIs, your variant systems, your composition patterns. The source of truth for a design system is shifting from Figma to code, and the designers who understand that will have a real advantage over those who don't.

Some decisions are expensive to change later. Token architecture, routing structure, and data models fall into this category. Others, like copy, spacing values, and colour choices, are cheap to change. I try to get the expensive ones right early and defer the cheap ones to the browser, where I can evaluate them against real content.

The first feature you build matters more than the others, because everything that follows inherits its patterns. Get the component structure and conventions right the first time and AI will replicate them across the rest of the project.

With that in mind, here's the process.

1. Write before you design

Every project starts with a plain text document. Not a formal specification, just a way to force clarity on my own thinking. I write a lightweight PRD that covers what the product does and why it needs to exist, what the core user flows are, what the hard constraints and open questions are, and what the product is explicitly not.

That last one matters more than people think. The non-goals section prevents scope drift later and gives AI a boundary to respect when generating features.

I describe flows in plain language rather than drawing them. "The user creates an account, connects their website, and receives a generated theme based on their brand" is more useful at this stage than a wireframe. Wireframes can look complete while quietly avoiding hard questions like what happens when there's no data.

AI is useful here as a thinking partner. I describe my idea conversationally and ask it to play devil's advocate. I let it ask me the questions I haven't thought of. It's good at surfacing edge cases and identifying technical constraints I might have missed.

This step takes an afternoon. If it's taking longer, I'm over-investing.

2. Make the foundational technical decisions

Before opening any tool, I make a small number of decisions that are expensive to change later.

The framework choice is really a question about what the product needs. If it's mostly client-side interactivity with no SEO requirements, Vite with React Router is simpler and faster. If I need server rendering, API routes, or a backend layer, Next.js is the right call. I don't default to Next.js out of habit. It adds real complexity that's only justified when you actually need what it provides.

For styling, Tailwind is my default. It maps directly to design token thinking, it's the system AI is most fluent in, and it eliminates the context-switching of separate CSS files. I also have AI set up a layer of CSS custom properties underneath Tailwind, so that utility classes like bg-primary resolve to semantic tokens that remap automatically between themes. It's the kind of configuration I could write myself, but don't need to anymore. I just need to know what to ask for and be able to verify that it's right.

The data model is the decision most designers skip and most regret skipping. Before designing a single screen, I sketch the shape of the data. What are the entities? What are their relationships? What does a typical API response look like? Even a rough JSON sketch prevents you from designing interfaces that conflict with the data they need to display. AI is strong here. Describe your product and ask it to propose a data model. You'll learn about your own product by reacting to what it suggests.

As an example: say you're building a project management tool. Your designer instinct is to start with the board view. But if you ask AI for a data model first, it'll come back with questions you hadn't considered. Does a task belong to one project or many? Can a user be on multiple teams? Are comments on tasks or on the project? These aren't visual design questions, but every one of them affects the interface you'll build. If you'd designed the task detail screen first, you might have assumed a single project context and then had to redesign it later when you realised tasks need to live across projects.

If the app has user accounts, I pick an auth approach now. Personally, Supabase for most projects. Rolling your own is almost never worth it at the validation stage.

The pitfall at this stage is over-engineering. I just need the key decisions that affect the shape of the code. Everything else can wait.

3. Design the system, not the screens

This step is about making visual decisions: type scale, spacing, colour, core component patterns. It happens before any code exists, and I want to explain why, because I used to do it differently.

My initial instinct when experimenting with AI was to build a working prototype first with generic styling, then layer in the visual design on top. Structure first, look-and-feel second. In practice, every visual decision sent ripples back through the prototype. Changing the type scale broke component sizing. Swapping in a real colour palette meant rethinking density and whitespace. Retrofitting a proper token system meant touching nearly every component. I was doing the work twice.

Visual system decisions are structural, not cosmetic. Your spacing scale, type scale, radius tokens, colour semantics. These shape how components are built, not just how they look. Get them wrong early and you'll be rebuilding things you thought were done.

The next question is where those decisions should live. For most of the industry's history, the answer was Figma. That's changing. When the thing you're building is code, and the tool building it is AI that works in code, maintaining a parallel design system in a design tool becomes overhead rather than infrastructure.

I still use Figma for the freeform, exploratory work: testing colour pairings, sketching component anatomy, trying type scales side by side. But Figma is a lossy format. Everything you decide there has to be re-described to AI in words, and things get lost in that translation. I've been using Paper (paper.design) more and more to bridge that gap. Paper's canvas is built on HTML and CSS, and AI agents can connect to it directly via MCP (in my experience, Figma's MCP is more error prone). Design decisions don't need to be translated into a prompt. They're already in a form AI can work with. Figma to think loosely, Paper to express decisions precisely.

This step produces decisions. A fluid type scale (typescale.com is my go-to). A spacing scale based on a 4px base unit. A colour palette structured around a three-layer token model: primitives, semantic tokens, and component-scoped tokens. Core primitive decisions for buttons, inputs, cards, navigation. These don't need to be polished. They need to be resolved enough that I can implement them confidently in the next step.

AI is mostly peripheral here. Taste decisions are mine to make.

4. Scaffold the project

This is where the repository comes into existence and where AI starts doing the heavy lifting.

I scaffold with precise instructions based on everything I've decided so far. Not "create a Next.js app" but something like "scaffold a Next.js App Router project with Tailwind v4. Look at the type scale and colour system in the selected frame in Paper and set them up as CSS custom properties using the ---- convention. Create the folder structure with ui/, patterns/, and feature component directories, and add a root layout using the type stack." The more specific I am up front, the less I need to fix afterwards.

My folder structure separates components by abstraction level:

src/
  app/                  # Routes or pages
  components/
    ui/                 # Design system primitives (Button, Input, Card)
    patterns/           # Composed patterns (Header, Sidebar, DataTable)
    [feature]/          # Feature-specific components
  lib/                  # Utilities, API clients, formatters
  hooks/                # Shared custom hooks
  styles/               # Global styles, token definitions
  types/                # Shared TypeScript types

ui/ holds the design system primitives: reusable, context-agnostic components. patterns/ holds compositions of those primitives that have opinions about layout. Feature folders hold components scoped to a single area of the app. The hierarchy matches how I think about design generally: primitives, patterns, instances.

The most important work in this step is token setup. I implement the visual decisions from Step 3 as a three-layer CSS custom property model. Primitives at the bottom (the raw palette), semantic tokens in the middle (what each value means), and component-scoped private properties at the top. I do the same for spacing, type, radius, shadow, z-index, and animation. Once these are in the codebase, they're the source of truth. Not the Figma explorations, not a spec document. This is where the design system actually lives.

I configure Tailwind to consume the semantic tokens so that bg-primary resolves to var(--color-primary), which resolves to the right primitive for the current theme. That one configuration step is what makes theming work across the entire codebase without any component needing to know what theme it's in.

I also set up the cn() utility (clsx plus tailwind-merge) and establish naming conventions: PascalCase for components, camelCase for utilities, kebab-case for routes. I document these in a Claude Code skill file so AI follows them automatically.

This should take a focused session, not a week. If I'm agonising over ESLint rules, I'm spending time on things I can refine later.

5. Build the shell

Before any features, I build the skeleton. The layout, the navigation, the routing structure. Everything else hangs on this.

The root layout comes first: the overall page structure, persistent elements like a sidebar or header, and the responsive behaviour. I build this with AI assistance rather than delegating it entirely, because the layout is where design judgement matters most. How the sidebar collapses on mobile, how the content area responds to different widths, how the navigation communicates the information architecture. These are design decisions, and they need a designer's eye even when AI is writing the code.

Then routes. I set up the pages that correspond to my core user flows. They can be empty shells with placeholder content. The point is to get the navigation skeleton in place so that features have somewhere to live.

The layout components are my first real components, and they follow the conventions from the start: accept className, forward ref, spread rest props. These early patterns propagate through everything that comes after.

I try not to build features before the shell is stable. If the layout changes significantly after features exist, you end up adjusting things that shouldn't have been affected.

6. Build features vertically

This is where the real building happens.

I build each feature end to end before moving to the next. The data layer, the components, all the states, the interactions. One complete vertical slice at a time. This is the opposite of building horizontally, where you'd create all the UI first, then wire up data, then add interactivity. Vertical slices give me something usable after each feature, which means I learn from real usage rather than waiting until everything's connected.

The first feature gets the most attention, because this is where component patterns get established. How I structure my first form, my first data display, my first loading state — these become the template AI replicates for every subsequent feature. If my first Button uses CVA for variants, merges className with cn(), forwards ref, and follows consistent prop conventions, then when I ask AI to build an Input it will follow the same patterns because they exist in the codebase as examples.

For later features, I can work at a higher level. "Build a settings page with sections for profile, notifications, and billing, following the same patterns as the dashboard." AI produces output that's consistent with what's already there, because the codebase itself is the specification it's working from.

State design is an underrated part of this. For every component I explicitly think through the loading state, the error state, the empty state, and the populated state at realistic volumes. What happens with one item? Fifty? Five hundred? This is where most AI output falls short unless you ask for it specifically. And it's where design skill makes the biggest difference in the final product.

The collaboration model shifts as features accumulate. For the first feature I lead closely, reviewing everything against the conventions. For later ones I describe at a higher level and review for quality. I find myself directing more and implementing less with every feature I build.

7. Refine in the browser

This is the step most people skip, and it's the one that makes the biggest difference to how the final product feels.

AI can generate structurally correct code, but it can't tell whether a transition is too fast, whether the spacing between sections feels right, or whether a loading skeleton is the right approach versus a simple fade-in. These are judgement calls that need real eyes on a real interface.

I use DevTools as a design tool and Agentation (https://agentation.dev/) to annotate the changes. Adjust values live, find what feels right, commit the changes. It's faster than edit-save-refresh and gives immediate feedback. I pay attention to vertical rhythm, type hierarchy, how the semantic tokens feel in context rather than in isolation, responsive behaviour at real device widths, and animation timing.

I also test edge cases visually. Long strings, missing images, single-item lists, maximum-content scenarios, narrow viewports, reduced motion. These are what expose whether the design system actually holds up or just looks good in the happy path.

AI is useful here for generating stress-test data and for targeted audits. "Check this component's accessibility." "Find spacing values that aren't on the scale." "Review this animation for performance." Specific, bounded requests against my established standards.

I try to budget real time for this. It's tempting to skip it, but the refinement pass is what makes the difference between something that feels considered and something that feels generated. The opposite problem is refining forever. I set a time box, fix the highest-impact things, and ship.

The rhythm

Each of these steps asks for a different kind of thinking, and I've found it helps to be deliberate about that.

Writing the PRD and making technical decisions is about exploring broadly and then committing. No code yet, just thinking and deciding.

Designing the system is the creative part. I don't try to be fast here because taste decisions need time to settle.

Scaffolding and building the shell are about precision. These are foundations and they need to be right.

Building features is where I iterate. Each one teaches me something about the architecture and the product.

Refining is where I trust my design instincts. If something looks off, it usually is.

After each project I write down what worked and what didn't. The process keeps changing. That's the point.