Tips for getting LLMs to write good UI code
TL;DR - use design system best practices, context engineering, and CI/CD tooling
Lots of teams are vibe coding frontend right now. Even with component libraries like shadcn, the UI can diverge quickly and make your app feel messy and amateurish. Inconsistency is a huge hindrance to good design, and LLMs on their own don’t do design consistency well.
You can get great UI out of LLMs, but it requires using good design principles, good tooling, and context management.
Fort the past few years, I’ve been working with Series-A startups teams to build their product UI using agents in Cursor and Claude code. Here are a few helpful lessons.
(PS - you can hire me to do this for you—details below)
Start with separation of concerns
You should have UI components in a separate design system. These files should be adjacent to, but distinct from, the application logic. Ideally use a top-level directory, or package in a monorepo.
Start with a UI library like shadcn, Radix, Untitled, or Base UI and adapt it to your needs.
Start with UI libraries, create opinionated design systems
Lots of UI libraries give devs a lot of flexibility in how they’re used. Libraries like shadcn are explicitly designed to be infinitely flexible, and their APIs support flexible composition and per-component styling.
This is a problem for design systems because it allows for inconsistency. Your app doesn’t need 10 flavors of a nav sidebar—pick the one you need and only let LLMs use that one. This is basically the difference between a UI library and a design system.
Document the design system
You want both LLMs and engineers to know what components are available. You can use Storybook, but I usually just make a secret page in the app with all components . This will be useful in the steps below.
Specifically, use a markdown component index file
Make one markdown file that lists and explains all UI components. Do not require LLMs to navigate the file system to understand what components are available. This file should be small enough for any reasonable context window.
Prompt LLMs to use the design system for every feature
Use agents.md to reference your component index: “Use the design system in /design-system for all UI components. Components.md has a list of all available components. Do not write new UI components inline.”
Configure brand tokens in one place
This is a bigger topic, but you should use proper (ideally semantic) variables for all color tokens using the preferred config method of your component library (tailwind, shad etc).
The idea is to define stuff like an error color once so the LLM doesn’t have to reinvent the wheel. This is good design practice, but important for LLMs to so they don’t waste tokens on writing boilerplate.
You can use a Figma extension to copy tokens right from Figma as CSS/JS and paste into your app.
Avoid LLM footguns
For example, shadcn allows you to pass custom a className to components by default. LLMs tend to use this to skirt design system rules and hack components for bad one-off UI.
I suggest removing this prop altogether. Prefer semantic props like variant="destructive" over className="bg-red-500". LLMs will use whatever props are available to them, so design your component API accordingly.
Use linters to enforce UI compliance!
LLMs don’t always get it right on the first shot, but if you give them a linter they can fix their own mistakes. Linters are less opinionated and more formal than prompts—think “trust but verify.”
Example lint rules: always use semantic color classes, always use sentence case, use heading components over literal text sizing for large text, never hit the network or use application logic in components. (LLMs are great at writing custom linter rules too)
Encourage design system usage in PR checks too
I recently wrote a Greptile rule to check all PRs for non-design system code and suggest changes if the system wasn’t used.
Make adding new components easy
Encourage developers and LLMs to add new components if their use case isn’t supported in the design system yet.
But quarantine new UI
Some new components are going to be whacky, and that’s OK. Instruct non-designer devs and LLMs to add a quarantine tag until a human designer reviews it and manually removes the tag. Use the smallest governance model that works for your design system and team.
Track progress visually in your app with a custom debug mode
I include a unique CSS class in all my design system component files. My linter requires this class, and prevents any code that’s outside the design system from using it.
Then, I add a secret debug mode to the app where I can toggle custom styling for the class and easily see what components are coming from the design system and which are not.