Design for AI-built apps: making it not look AI-made
AI-built apps look alike because the tools reach for the same components, neutral palettes, fonts and layouts. Fix it by deciding the product's own type, colour, spacing and voice as design tokens, then designing the flows and states the tools skip: empty, error, loading, onboarding and mobile.
On this page
- In this guide
- Why AI-built apps look the same
- Start from the product, not a style
- Make the tokens yours
- Customise shadcn/ui without fighting it
- Fix the UX the tools skip
- Words are part of the design
- Design the states nobody prompts for
- Onboarding to the first useful moment
- Dashboards that answer a question
- A landing page that says one thing
- Accessibility is part of the design
- When to bring in a designer
In this guide
This is the hub for everything on the blog about design for apps built with AI tools. Each section is a summary with a link down to the full article.
- Why they look the same. Where the generic look actually comes from.
- Start from the product. Picking a direction before picking colours.
- Your own tokens. Colour, type, spacing and radius as decisions, with code.
- Customising shadcn/ui. Changing the look without fighting the library.
- The UX the tools skip. Flows, hierarchy and navigation.
- Words. Interface copy as part of the design.
- Empty, error and loading states. The screens nobody prompts for.
- Onboarding. Getting a new user to the first useful moment.
- Dashboards. What to show, and what to leave out.
- Landing pages. Saying one thing clearly.
- Accessibility. WCAG 2.2 basics that fit in an MVP.
- When to bring in a designer. And what to expect from one.
Why AI-built apps look the same
Lovable, Bolt, v0 and the rest are very good at producing a working interface quickly. For a prototype, that is exactly what you want. The trouble is that "working" and "designed for this product" are different things, and the gap shows the moment someone compares your app with the last three they saw.
Most of the sameness comes from defaults. v0's own docs say it uses shadcn/ui as its default component system, and any app built on shadcn/ui and Tailwind CSS starts from the same default theme unless someone changes it. That theme is a set of CSS variables: a neutral background, a near-black primary, a --radius of 0.625rem, and base palettes named Neutral, Stone, Zinc, Mauve, Olive, Mist and Taupe. They are well made, which is exactly why they turn up everywhere. When every product starts from the same tokens and nobody changes them, every product looks related.
Then come the habits. Ask any of these tools for a SaaS landing page or a dashboard and you tend to get the same moves:
- A centred hero with a gradient headline and two buttons
- Three feature cards in a row, each with an icon in a tinted circle
- Everything inside a rounded card with a soft shadow, including things that are not cards
- One accent colour, often in the purple to blue range, used on everything that needs attention
- Small uppercase labels with wide letter spacing above every heading
- Placeholder copy like "Streamline your workflow" that could describe any product
- Even padding everywhere, so nothing is grouped and nothing stands out
None of these is wrong on its own. Together, with no decisions behind them, they read as "made by a tool". Users may not be able to say why, but they notice, and for a paid product that costs trust.
The full list of tells, and where each comes from, is in why AI apps all look the same.
Start from the product, not a style
The fix is not "add more colour" or "use a different template". A different template is still a template. The fix is to make the decisions the tool made by default, on purpose, from what the product is and who uses it.
Before touching any CSS, I answer four questions:
- Who uses it, and where? A tool for site managers checked on a phone in the rain wants big targets and high contrast. A tool for analysts on a wide monitor can be dense.
- How often? Something used daily should be calm and fast to scan. Something used once a month needs more explanation on screen.
- What should it feel like? Pick three plain words. "Calm, precise, trustworthy" leads somewhere different from "warm, playful, local".
- What is the one thing on each screen? If every screen has one obvious main action, half the hierarchy problems go away.
Those answers become the direction, and the direction decides the tokens. FindPopups, my own marketplace for street food and markets, is local, outdoors and mostly used on a phone, which pushed it towards bold type, warm colours and a map at the centre of the product, not a dashboard of cards. The FindPopups case study shows how those choices played out.
Make the tokens yours
Design tokens are the named values the whole interface is built from: colours, fonts, sizes, spacing, radius, shadows. In a Tailwind and shadcn/ui project they live in one CSS file, which is good news: change them there and every screen changes with them.
Colour
shadcn/ui pairs each surface token with a foreground token for the text on top of it: --primary with --primary-foreground, --muted with --muted-foreground, and so on. The defaults use oklch() values, and a .dark selector overrides the same tokens for dark mode. The new values are then exposed to Tailwind through @theme inline, which is what makes utilities like bg-primary and text-muted-foreground work.
A trimmed example of what that looks like once it is yours:
/* app/globals.css: trimmed; the shadcn CLI generates the full token list */
@import "tailwindcss";
@custom-variant dark (&:is(.dark *));
:root {
--radius: 0.375rem;
--background: oklch(0.985 0.006 85); /* warm off-white, not pure white */
--foreground: oklch(0.22 0.02 60);
--primary: oklch(0.45 0.12 150); /* a deep green chosen for the brand */
--primary-foreground: oklch(0.98 0.01 150);
--muted: oklch(0.95 0.01 85);
--muted-foreground: oklch(0.48 0.02 60);
--border: oklch(0.89 0.01 85);
--ring: oklch(0.45 0.12 150);
}
.dark {
--background: oklch(0.2 0.01 60);
--foreground: oklch(0.95 0.01 85);
--primary: oklch(0.75 0.12 150);
--primary-foreground: oklch(0.2 0.02 150);
--muted: oklch(0.27 0.01 60);
--muted-foreground: oklch(0.72 0.02 85);
--border: oklch(0.32 0.01 60);
--ring: oklch(0.75 0.12 150);
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-border: var(--border);
--color-ring: var(--ring);
--radius-lg: var(--radius);
}
/* Check every text and background pair with a contrast checker before shipping. */Three habits make colour work harder for you. Use fewer colours than you think: one brand colour, a neutral range and the status colours is plenty. Keep the brand colour for the main action, so it means "do this" rather than "decoration". And tint your neutrals slightly towards the brand, so greys feel like part of the same product instead of a default.
If you want to stop the default Tailwind palette creeping back in through a stray bg-blue-500, Tailwind CSS v4 lets you clear a whole namespace in @theme and keep only your own values:
@theme {
--color-*: initial;
--color-white: #fff;
--color-black: #000;
/* then your own named colours */
}Tailwind's default palette has 26 colour families, each with 11 steps from 50 to 950, defined in OKLCH. That is a lot of choice to leave open in a product that should only use a handful.
Type
Typography carries more of a product's character than colour does. A deliberate typeface, a small number of sizes and real hierarchy between them will do more than any gradient.
In Next.js, next/font downloads Google Fonts at build time and serves them with your other static files, so the browser sends no requests to Google. The variable option gives you a CSS variable to hand to Tailwind:
// app/layout.tsx
import { Fraunces, Inter } from "next/font/google";
// Both are variable fonts, so no weight is needed. A static font needs a `weight` option.
const inter = Inter({ subsets: ["latin"], display: "swap", variable: "--font-inter" });
const fraunces = Fraunces({ subsets: ["latin"], display: "swap", variable: "--font-fraunces" });
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en-GB" className={`${inter.variable} ${fraunces.variable}`}>
<body className="font-sans">{children}</body>
</html>
);
}/* app/globals.css */
@theme inline {
--font-sans: var(--font-inter);
--font-display: var(--font-fraunces);
}That gives you font-sans for body text and font-display for headings. The specific pair matters less than having a reason for it. A serif display face with a plain sans body says something different from a single geometric sans, and both say more than whatever the tool picked.
Then fix the scale. Pick five or six sizes and use only those. Most generic interfaces have headings that are barely bigger than body text and labels that are barely smaller, so nothing leads the eye.
Spacing and radius
Tailwind v4 derives its spacing utilities from one variable, --spacing, which defaults to 0.25rem, so p-4 is 1rem. You do not need to change the base unit. You need to use the scale with intent: tight spacing inside a group, generous spacing between groups. Even padding everywhere is one of the clearest tells of an undesigned screen, because it flattens the structure.
Radius is a small token with a big effect. shadcn/ui derives a whole radius scale from --radius. A tighter radius reads as more precise and serious; a larger one as softer and friendlier. Pick one and apply it everywhere, including inputs and images.
The full approach, including how few tokens an MVP really needs, is in a design system for your MVP.
Customise shadcn/ui without fighting it
shadcn/ui describes itself as "not a component library" but the way you build your own. The component code is copied into your project, and the docs say the top layer is open for you to change. That is the key to making it yours: you are expected to edit the files, not wrap them in overrides.
In practice that means three levels of change, from cheapest to most effort:
- Tokens. Change the CSS variables above. Every component picks up the new colour, radius and focus ring at once.
- Variants. The generated button file defines its looks as named variants. Add the ones your product needs and delete the ones it does not.
- Structure. Edit the component markup itself when the default pattern does not fit, such as a dialog that should be a full page on mobile.
A variant change looks like this in the button file the CLI generated:
// components/ui/button.tsx: excerpt, check against the file in your project
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
quiet: "text-foreground underline underline-offset-4 hover:text-primary",
destructive: "bg-destructive text-white hover:bg-destructive/90",
},
size: {
default: "h-11 px-5",
sm: "h-9 px-3 text-sm",
},
},
defaultVariants: { variant: "default", size: "default" },
}
);The buttons on this site are built that way: shadcn/ui underneath, with my own variants, sizes and radius on top. Nobody looking at them would guess the starting point, and the components still update and behave the same way.
What to avoid: piling className overrides onto every use of a component. It works for one screen and turns into a mess by the tenth. If you are overriding the same thing three times, change the component or the token instead.
The step by step version, including dark mode and adding your own tokens like --warning, is in how to customise shadcn/ui.
Fix the UX the tools skip
Visual sameness is the easy part to spot. The more expensive problems are in the experience, because AI tools build the screens you describe, and founders mostly describe the happy path.
The patterns I see most often in AI-built apps:
- No clear main action. Three buttons of equal weight at the top of a screen, so the user has to read all of them to decide.
- Navigation that mirrors the database, not the job. A menu with "Users", "Items", "Records" instead of the three things a person actually comes to do.
- Forms that ask for everything up front. Twelve fields before the user has seen any value.
- Copy that explains nothing. "Submit", "Manage", "Settings" on everything. Button labels should say what happens: "Send invoice", "Publish listing".
- Modals for everything. Long forms in a dialog that cannot scroll properly on a phone.
- Desktop-only layouts. Tables that overflow, side panels that cover the content, and tap targets sized for a mouse.
Most of these fixes cost little in code. They cost a decision, which is exactly what the tool could not make for you. UX mistakes AI tools make goes through each with a before and after. The mobile layout problems in particular have their own fix guide, AI app mobile layout.
Words are part of the design
Generic copy does as much to make an app feel AI-made as generic colour, and it is quicker to fix. The tools write plausible text for whatever they cannot know: your product's name for things, your users' words for their problems, your tone. So you get "Welcome back" on every dashboard, "Get started" on every button and "All your tools in one place" in every feature card.
A short pass through every screen, reading only the words, catches most of it. What I look for:
- Labels that say what happens. "Publish listing" instead of "Submit". "Cancel subscription" instead of "Confirm". The user should know the result before they click.
- One name for each thing. If it is a "booking" on one screen, it is not a "reservation" on the next and an "order" in the email.
- Your users' words, not the database's. People do not have "entities" or "records". They have clients, jobs, listings, invoices.
- Helper text that helps. Explain the field that actually confuses people, such as what happens to a phone number, and leave the obvious ones alone.
- Errors in plain English. What went wrong, whether anything was lost, and what to do now. Never a raw error code on its own.
- A consistent voice. Pick how formal the product is and keep to it, from the landing page to the password reset email.
I write the copy alongside the design rather than after it, because a label that needs three words instead of one changes the layout. If a screen is hard to write, it is often a sign the screen itself is doing too much.
A useful test: cover the logo and read the main screen out loud. If it could belong to any other product, the words are not finished.
Design the states nobody prompts for
Every screen that shows data has at least four states: loading, empty, error and full. AI tools reliably build the full one, because that is what the prompt described and what the demo data shows. The other three are where a product feels finished or not.
- Empty. The first thing a new user sees. A blank table with "No data" is a missed chance to explain what goes here and give one action to fill it.
- Loading. A skeleton shaped like the content that is coming is calmer than a spinner in the middle of the page, and it stops the layout jumping.
- Error. Say what went wrong in plain words, whether the user's work is safe, and what to do next. "Something went wrong" answers none of those.
- Partial. One item, or a thousand. Both break layouts designed around exactly six.
Status messages also need to reach people using screen readers. WCAG 2.2 calls this 4.1.3 Status Messages: a message like "Changes saved" should be announced without moving focus to it. A role="status" region does that:
// A save confirmation that screen readers announce without stealing focus
export function SaveStatus({ saved }: { saved: boolean }) {
return (
<p role="status" className="text-sm text-muted-foreground">
{saved ? "Changes saved" : ""}
</p>
);
}Keep the element on the page and change its text, rather than adding and removing the whole element, so assistive technology picks up the change.
The full set of patterns, with copy you can adapt, is in empty, error and loading states. If the errors themselves are the problem, rather than how they look, the fix pillar covers error handling in AI-generated code.
Onboarding to the first useful moment
Onboarding is not a product tour. It is the shortest path from sign-up to the moment the product does something useful for this person. Every step between those two points is a place to lose them.
What that means for an MVP:
- Ask only what you need now. Name and email to sign up. Anything else can wait until the moment it is needed.
- Start with something in the account. An example project, a template, or a pre-filled first item beats an empty screen with a "Create" button.
- Use the empty states. Done well, they are most of your onboarding, and they stay useful after the first visit.
- Show progress honestly. A three-item checklist the user can actually finish is better than a tour of eight tooltips they will click past.
- Measure it. Know how many sign-ups reach the first useful moment, and where the rest stop.
AI tools tend to either skip onboarding entirely or produce a multi-step welcome carousel. Neither is designed around your product's first useful moment, because only you know what that is. App onboarding best practices walks through how to find it and design towards it.
Dashboards that answer a question
The default AI dashboard is a row of four stat cards, a line chart and a table, whatever the product. It looks like a dashboard. It rarely answers the question the user opened it to ask.
A good MVP dashboard starts from that question. For a vendor on a marketplace, it might be "where am I trading next, and has anything changed?" That is a list of upcoming dates with changes highlighted, not a chart of page views. For a team lead in a SaaS tool, it might be "what needs my attention today?" That is a short queue, not four numbers.
Rules I use:
- One question per dashboard. If there are three, there are probably three views.
- Actions next to information. If a number tells someone to do something, the button to do it should be right there.
- Numbers with context. "42 bookings" means nothing; "42 bookings, up from 30 last week" means something.
- Fewer charts. A chart is worth it when the shape over time matters. Otherwise, a number or a sentence is clearer.
- Tables that work on a phone. Either collapse rows into cards on small screens or pick the two columns that matter.
The longer version is dashboard design for MVPs. For a worked example, the teardown of a vibe-coded dashboard takes a typical AI-generated dashboard and redesigns it screen by screen.
A landing page that says one thing
AI-generated landing pages have a recognisable structure: big hero, three feature cards, logo row, testimonials, pricing, FAQ, footer. It is a reasonable skeleton. The problem is that the words inside it are interchangeable, so the page could be selling anything.
What makes a landing page work is mostly writing:
- A headline that names the person and the outcome. Not "The future of scheduling" but who it is for and what changes for them.
- One main action. Sign up, book a demo or join the waitlist. Not all three with equal weight.
- Proof that is specific. A real screenshot of the product, a named customer, a concrete number you can stand behind. If you do not have proof yet, show the product instead of inventing it.
- Objections answered. Price, setup time, what happens to my data. The FAQ is where people decide.
Design follows the words. Once the message is clear, the layout can be simpler than the template, and simpler usually reads as more confident. A SaaS landing page that converts covers structure, copy and the common mistakes in more depth.
Accessibility is part of the design
Accessibility is often treated as a compliance task for later. For an MVP, the basics are a handful of design decisions that are cheap now and expensive to retrofit, and they make the product easier for everyone to use.
WCAG 2.2, the current W3C Recommendation, is the reference. The criteria that catch AI-built apps most often:
| Level | What it asks for | Common miss in AI-built apps | |
|---|---|---|---|
| 1.4.3 Contrast (Minimum) | AA | Text contrast of at least 4.5:1, or 3:1 for large text | Light grey helper text and placeholder-style labels |
| 1.4.11 Non-text Contrast | AA | At least 3:1 for UI components and meaningful graphics against adjacent colours | Input borders and icons too faint to see |
| 2.4.7 Focus Visible | AA | A visible keyboard focus indicator | Focus rings removed with outline-none and nothing put back |
| 2.4.11 Focus Not Obscured (Minimum) | AA | The focused element is not entirely hidden by author-created content | Sticky headers and cookie banners covering the focused field |
| 2.5.8 Target Size (Minimum) | AA | Pointer targets at least 24 by 24 CSS pixels, with exceptions such as enough spacing | Small icon buttons packed together in tables |
| 1.4.10 Reflow | AA | Content works at a width equivalent to 320 CSS pixels without scrolling in two directions | Wide tables and fixed-width layouts on phones |
| 3.3.1 Error Identification | A | Detected input errors are identified and described in text | Errors shown only as a red border |
| 3.3.8 Accessible Authentication (Minimum) | AA | No cognitive function test, such as remembering a password, without an alternative or assistance | Paste blocked in password or one-time code fields |
A few notes from the W3C's own guidance. Large text, for the contrast criterion, means at least 18 point, or 14 point bold. For accessible authentication, supporting password managers and allowing paste both count as help, and the guidance flags one-time code fields split into single digits that will not accept a pasted code as a problem. Email link sign-in is listed as a sufficient technique.
In a shadcn/ui project, most of this comes down to tokens and a few habits: set --muted-foreground and --border dark enough to pass, keep the --ring focus style rather than removing it, make icon buttons at least the minimum target size, and connect each error message to its input. The accessibility guide for AI-built apps goes through a full pass, criterion by criterion, with code.
When to bring in a designer
You can get a long way on your own with the steps in this guide: decide the direction, set your tokens, fix the type, design the states, write real copy. For a prototype or an internal tool, that may be all you need.
It is worth bringing in a designer when:
- People are paying, or about to. Trust is part of the price, and generic design spends it.
- Users get stuck and you cannot see why. That is a flow problem, and flows are where design experience pays for itself.
- Every new screen makes the app less consistent. A small design system fixes this once.
- You are raising money or selling to businesses. The product is the demo.
If you hire a designer for an AI-built app, look for one who can work in the code, or at least read it. A Figma file that redesigns your app is only useful if someone can rebuild the screens in your actual components. A designer who codes can change the tokens, components and screens directly, in the same repository the AI tool generated, which is usually faster and cheaper than a full redesign followed by a rebuild. I explain what that role is, and when it is the wrong choice, on the designer who codes page.
That is the work I do. You can see real products I have designed and built on the work page, and if you want to cut down what you are asking for before talking to anyone, scope your MVP first.
The articles in this pillar, in the order most founders need them:
- Why AI apps all look the same
- A design system for your MVP
- How to customise shadcn/ui
- UX mistakes AI tools make
- Empty, error and loading states
- App onboarding best practices
- Dashboard design for MVPs
- Teardown of a vibe-coded dashboard
- A SaaS landing page that converts
- Making an AI-built app accessible
- shadcn/ui docs: Introduction, checked 9 November 2026
- shadcn/ui docs: Theming, checked 9 November 2026
- v0 docs: Design systems, checked 9 November 2026
- shadcn/ui docs: Button, checked 9 November 2026
- Tailwind CSS docs: Theme variables, checked 9 November 2026
- Tailwind CSS docs: Colors, checked 9 November 2026
- Next.js docs: Font module, checked 9 November 2026
- W3C: Web Content Accessibility Guidelines (WCAG) 2.2, checked 9 November 2026
- W3C: Understanding 2.5.8 Target Size (Minimum), checked 9 November 2026
- W3C: Understanding 3.3.8 Accessible Authentication (Minimum), checked 9 November 2026
- W3C: Understanding 4.1.3 Status Messages, checked 9 November 2026
- W3C: Understanding 3.3.1 Error Identification, checked 9 November 2026
Questions founders ask
Why do all AI-built apps look the same?
The tools build on the same few component libraries and start from their default themes, so the same neutral palette, radius, font and card layout turn up everywhere. Nothing is wrong with the defaults. They just were not chosen for your product.
Can I make a shadcn/ui app look custom without rewriting it?
Yes. shadcn/ui components are copied into your project and read their look from CSS variables, so changing the tokens in your global CSS and editing the component files changes the whole app without replacing it.
What is the quickest single change that makes an AI app look less generic?
Usually typography: a deliberate typeface, a clear size scale and real copy instead of placeholder phrases. Colour changes help, but type and words carry more of the character.
Do I need a design system for an MVP?
You need a small one: a handful of colour tokens, a type scale, a spacing scale, a radius and your core components. That is enough to keep new screens consistent without slowing the build down.
Is accessibility worth doing at MVP stage?
The basics are cheap if you do them as you go: contrast, visible focus, labelled inputs, readable errors and decent tap targets. Retrofitting them later costs far more, and they make the product better for everyone.

Ant
Product designer who builds MVPs, based in Nottingham. 10+ years designing digital products, and the only person at BlackFig. More about me