# How to Make UI Not Look AI-Generated: A v0 Checklist

> To make UI not look AI-generated, change the defaults v0 and shadcn ship with: your own color, background and radius tokens, two typefaces with different jobs, a left-aligned asymmetric hero instead of a centered gradient, no row of three icon cards, varied spacing, and one or two interactions with real feedback.

- URL: https://qodesign.dev/blog/make-v0-app-not-look-ai-generated
- Author: Teodor Avadani (https://qodesign.dev/blog/author/teodor-avadani)
- Published: 2026-09-29
- Updated: 2026-10-20
- Category: AI-built UI
- Publisher: qodesign (https://qodesign.dev), a shop of animated React + Tailwind CSS components for Next.js, each sold with its full source code and an AI prompt.

To make UI not look AI-generated, change the defaults your tool ships with. In a v0 app that means: replace the stock shadcn theme tokens with your own colors and radius, pick two typefaces that have different jobs, break the centered gradient hero and the three icon cards, and add one interaction that feels intentional instead of fading everything in. None of this needs a redesign. Most of it lives in `globals.css`, one font file and two components.

People spot generated UI from a few patterns repeated across thousands of apps. A lot of advice on this stops at "use better prompts" or "add personality". Below is each pattern, why it shows up, the code I use to replace it in a Next.js App Router project with Tailwind v4 and shadcn/ui, and a small script that counts the tells in your own repo. Every snippet here was built in a fresh `create-next-app@15` project with `shadcn init` and passes `tsc --noEmit` and `next build`.

## Why do all AI-generated websites look the same?

v0 generates Next.js with Tailwind and shadcn/ui. That stack is a good choice, and the problem is only that nobody changes the starting values. A fresh `npx shadcn@latest init` writes a neutral grayscale palette and `--radius: 0.625rem` into `globals.css` (I checked the file it wrote in my test app). Every app built on those values shares the same gray buttons, the same corner radius and the same muted text color.

So the job is to keep the components and change what they read from.

There's one more reason to work at the token level rather than rewriting components. v0's own docs on custom registries say it is ["specifically trained on the default implementations of the shadcn/ui components and may struggle with any customizations"](https://v0.app/docs/design-systems-legacy). If you rewrite the internals of `button.tsx`, later generations can fight you. If you change the CSS variables those components read, v0 keeps generating the components it knows, and they come out in your colors.

## Tell 1: how do I change the default shadcn theme?

The giveaway is a black or near-black primary button, `neutral` grays everywhere and the same rounded corners on every card. The shadcn components read colors and radius from CSS variables, so you can restyle all of them from one place. The [shadcn theming docs](https://ui.shadcn.com/docs/theming) list every variable.

Here is the override I appended to the bottom of `src/app/globals.css`, after the block shadcn generated:

```css
/* qodesign-blog-tokens (article: make-v0-app-not-look-ai-generated) */
:root {
  --radius: 0.375rem;
  --background: oklch(0.985 0.008 85);
  --foreground: oklch(0.23 0.02 60);
  --primary: oklch(0.55 0.17 35);
  --primary-foreground: oklch(0.98 0.01 85);
  --muted-foreground: oklch(0.5 0.02 60);
  --border: oklch(0.89 0.015 80);
  --ring: oklch(0.55 0.17 35);
}
```

Three choices are doing the work here:

- The background is a warm off-white (a little chroma at hue 85) instead of pure `oklch(1 0 0)`. Text and borders get the same warm tint so the page reads as one material.
- The primary is an actual color. Pick it from your brand or your logo, not from a palette generator's "modern blue".
- The radius drops from 0.625rem to 0.375rem. Smaller corners read as more deliberate. If your brand is soft and friendly, go the other way and make them clearly rounder. The point is to pick a value on purpose.

Because shadcn's generated `@theme inline` block maps `--color-primary` to `var(--primary)` and derives `--radius-sm` to `--radius-4xl` from `--radius`, every `bg-primary`, `rounded-lg` and `border-border` in the app picks this up without touching a component.

## Tell 2: one sans-serif for everything

Default output uses a single sans (Geist from create-next-app, or Inter) for headings, body, buttons and labels. It's readable, and nobody remembers it.

Use two typefaces with different jobs: something with character for headings, and a plain workhorse for reading. I load them with `next/font`, which self-hosts the files and exposes each font as a CSS variable ([next/font docs](https://nextjs.org/docs/app/api-reference/components/font)):

```ts
import { Fraunces, IBM_Plex_Sans } from "next/font/google";

export const display = Fraunces({
  subsets: ["latin"],
  variable: "--font-display",
  axes: ["opsz"],
});

export const body = IBM_Plex_Sans({
  subsets: ["latin"],
  weight: ["400", "500", "600"],
  variable: "--font-body",
});
```

Put the variables on a wrapper (a layout is fine):

```tsx
import { display, body } from "./fonts";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <div className={`${display.variable} ${body.variable} font-sans`}>
      {children}
    </div>
  );
}
```

Then register them as Tailwind utilities in the same `globals.css`, using Tailwind v4's `@theme` ([Tailwind theme docs](https://tailwindcss.com/docs/theme)):

```css
@theme inline {
  --font-display: var(--font-display);
  --font-sans: var(--font-body), ui-sans-serif, system-ui, sans-serif;
}
```

Now `font-display` is a utility and `font-sans` (the default body font) is Plex. Fraunces and Plex are only an example. A grotesk paired with a mono works, and so does a condensed sans with a humanist one. What matters is that headings and body text don't look like the same font at two sizes.

## Tell 3: the centered gradient hero

A centered H1, a gradient blob behind it, a subtitle and two buttons side by side. This is the most recognizable generated layout there is.

Things that break it without much effort:

- Left-align the headline and let it run large. `text-balance` keeps the line breaks even.
- Cap the paragraph at a reading width (`max-w-[52ch]`) instead of centering it under the title.
- Use one primary button and a plain text link for the secondary action.
- Put a second, smaller element on the right of the grid, so the hero has an asymmetric layout instead of a centered axis.

Here is the hero from my test page:

```tsx
<section className="grid gap-10 md:grid-cols-[1.4fr_1fr] md:items-end">
  <div>
    <h1 className="font-display text-5xl leading-[1.05] tracking-tight text-balance md:text-7xl">
      Invoices your clients actually open
    </h1>
    <p className="mt-6 max-w-[52ch] text-lg text-muted-foreground">
      Send a link, get paid by card or transfer, and stop chasing people over email.
    </p>
    <div className="mt-10 flex items-center gap-6">
      <PressButton>Send your first invoice</PressButton>
      <Link href="#how" className="text-sm underline underline-offset-4">
        How it works
      </Link>
    </div>
  </div>
  <p className="border-l border-border pl-6 text-sm text-muted-foreground">
    Used by freelancers who would rather not explain bank details twice.
  </p>
</section>
```

No gradient. If you want color in the hero, use a real image of your product.

## Tell 4: three feature cards with an icon in a rounded square

Three equal cards, each with a lucide icon in a tinted rounded square, a bold two-word title and one line of text. It shows up because it's the safest way to fill a section.

Your features are probably not three equal things, and the icons rarely add information. A numbered list with dividers reads as written by someone, and it scales to two or five items without looking broken:

```tsx
<ol id="how" className="mt-32 divide-y divide-border border-y border-border">
  {changes.map((c, i) => (
    <li key={c.title} className="grid gap-2 py-8 md:grid-cols-[4rem_14rem_1fr]">
      <span className="font-display text-2xl text-primary">0{i + 1}</span>
      <h2 className="font-medium">{c.title}</h2>
      <p className="text-muted-foreground">{c.text}</p>
    </li>
  ))}
</ol>
```

The number in the display font is the only decoration, and it uses the brand color from Tell 1. If one feature matters more than the others, give it more space instead of forcing it into the grid.

## Tell 5: every block centered with the same spacing

Generated pages often use the same `py-16` or `py-24` on every section and center everything. The rhythm is flat, so nothing stands out.

Two rules I follow:

- Vary vertical space on purpose. The gap after the hero (`mt-32` above) is bigger than the gap inside a section, which groups related things and separates unrelated ones.
- Pick one alignment axis and stick to it. In the test page everything hangs off the left edge of a `max-w-5xl` container. Centering is fine for a single short CTA block, not for every section.

Spacing is where AI-built pages and designed pages differ the most, and fixing it costs nothing in bundle size.


![qodesign day/night theme toggle, the sun setting into a starry sky](/previews/day-night-toggle.jpg)

## Tell 6: no motion, or fade-in on everything

There are two opposite tells here. Some apps have no feedback at all, so buttons feel dead. Others fade and slide every section in on scroll, which looks like a template demo.

Pick one or two moments that matter and make them feel physical. The main button is a good first one. This one lifts a pixel on hover and presses down on click, using only Tailwind transitions, so it works in a Server Component with no JavaScript:

```tsx
export function PressButton({ children }: { children: React.ReactNode }) {
  return (
    <button
      type="button"
      className="rounded-[var(--radius)] bg-primary px-5 py-3 text-sm font-medium text-primary-foreground shadow-[0_1px_0_rgb(0_0_0/0.2),inset_0_1px_0_rgb(255_255_255/0.15)] transition-[transform,box-shadow] duration-150 ease-out hover:-translate-y-px active:translate-y-0 active:scale-[0.97] motion-reduce:transition-none motion-reduce:active:scale-100"
    >
      {children}
    </button>
  );
}
```

The `motion-reduce:` variants turn it off for people who set "reduce motion" in their OS ([prefers-reduced-motion on MDN](https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@media/prefers-reduced-motion)). The inset highlight in the shadow is a small detail, and small details like it are a big part of why a button looks designed.

When you need more than a press (a toggle that springs, a tab indicator that slides, a login form that reacts), that's where a ready-made piece saves time. The [micro-interactions](/ui/micro-interactions) and [animated login forms](/ui/animated-login-forms) collections have examples with the source code.

## How do I find the AI tells in my own code?

Reading every file is slow, so I start with a count. This loop greps the `.tsx` files under `src` for the class names and imports that show up most in default output: gradient backgrounds (`bg-gradient-to-*` in Tailwind v3, `bg-linear-to-*` in v4), purple and indigo stops, centered text, extra-round corners, lucide imports, the same big section padding, and the default fonts.

```bash
for p in 'bg-(gradient|linear)-to-' 'from-(purple|indigo|violet)-' 'text-center' 'rounded-(xl|2xl)' 'lucide-react' 'py-(16|20|24)' 'font-(inter|geist)'; do
  printf '%-30s %s\n' "$p" "$(grep -rEio "$p" src --include='*.tsx' | wc -l)"
done
```

I ran it on a small hero written the way generated code usually looks (centered, gradient, three icon cards) and got:

```txt
bg-(gradient|linear)-to-       1
from-(purple|indigo|violet)-   1
text-center                    4
rounded-(xl|2xl)               2
lucide-react                   1
py-(16|20|24)                  2
font-(inter|geist)             0
```

None of these is wrong on its own. A `text-center` on a short CTA is fine, and lucide is a good icon set. The count tells you where to look. If one file has four `text-center` lines and a purple gradient, that's the section to redo first, using the fixes above.

### Tells and fixes at a glance

| Tell | Fix | Where it lives |
| --- | --- | --- |
| Stock gray theme, same radius everywhere | Your own `--primary`, `--background`, `--radius` | `globals.css` |
| One sans for everything | A display face plus a body face | `next/font` + `@theme` |
| Centered hero on a gradient | Left-aligned, asymmetric grid, real product image | Hero component |
| Three icon cards | Numbered list with dividers | Features section |
| Same `py-24` on every section | Varied spacing, one alignment edge | Page layout |
| No feedback, or fade-in on everything | One or two physical interactions | Buttons, toggles |

## A quick checklist before you ship

Go through your v0 export with this list:

1. `globals.css` has your own `--primary`, `--background` and `--radius`, not the shadcn defaults.
2. Headings and body use different typefaces, loaded with `next/font`.
3. The hero isn't centered on a gradient.
4. No section is three identical icon cards.
5. Section spacing varies, and everything shares one alignment edge.
6. One or two interactions have real feedback, and they respect reduced motion.
7. Copy names your product and your user. "Supercharge your workflow" is a tell too.

You can paste this list into v0 or Cursor as a follow-up prompt. Include the exact tokens and font names from your `globals.css` instead of asking it to "make it look less generic", so it has something concrete to apply.

## Swapping in one distinctive component

Sometimes the fastest win is replacing one stock piece with something people haven't seen in a hundred other apps: a theme switch, a navigation bar, a checkout. If you want to try that without paying, the [/free](/free) page gives you one component with its full source, and a [day/night toggle](/components/day-night-toggle) is a good one to start with because every app has a theme switch.

I make animated React components at qodesign, and this checklist is what I run on every v0 project before I add any of them.

## FAQ

### How do I make UI not look AI-generated?

Change the defaults rather than the prompt. Set your own color, background and radius tokens, use two typefaces with different jobs, break the centered hero and the three-card row, vary your spacing, and give one or two controls real feedback.

### Why does every AI website look the same?

Most AI builders generate the same stack, Next.js with Tailwind and shadcn/ui, and almost nobody changes its starting theme. The result is the same grays, the same radius, the same font and the same section layouts across thousands of apps.

### Can I just prompt v0 to make it look less generic?

Vague prompts like "make it look less generic" rarely work because there's nothing concrete to apply. Give it the exact tokens, font names and layout rules from your own `globals.css`, or a screenshot of a design you like, and it has something specific to follow.

### Should I customize the shadcn components v0 uses?

Change the CSS variables first. v0's docs say it is trained on the default shadcn/ui implementations and may struggle with customized ones, so theming through tokens keeps later generations working while still changing how everything looks.
