DEV Community

Cover image for Make your v0 or Cursor app stop looking AI-built in 10 minutes
Teodor Avadani
Teodor Avadani

Posted on

Make your v0 or Cursor app stop looking AI-built in 10 minutes

If you've built a landing page with v0, Cursor, Lovable or Bolt, you've probably had this moment: you put it next to someone else's and it's the same page. Purple-to-blue gradient, centred headline, one font, three feature cards with an icon in a circle, a pricing table, and a "Get started" button that does nothing you can see.

Nothing on that page is broken. It's the average of every landing page the model has seen, and that's what you get when you don't ask for anything specific. Visitors recognise it within seconds, though, and some of them decide the product behind it got the same amount of thought.

Here are the five tells I look for and a fix for each. Every fix is a couple of minutes in a Next.js App Router project with Tailwind CSS v4. I put the result in a free starter (MIT) so you can copy single pieces or start from the whole thing:

The starter: signup toast, theme toggle, pricing switch

The five tells

  1. A gradient where a brand colour should be.
  2. One font at one weight for everything.
  3. Everything centred.
  4. An illustration where the product should be.
  5. Buttons that don't respond.

0. Get a page to work on (1 minute)

To try the fixes on a clean page:

npx create-next-app@latest my-app -e https://github.com/teodoravadani/qodesign-next-starter
cd my-app
npm run dev
Enter fullscreen mode Exit fullscreen mode

Or open your own project. The steps assume Next.js 15 (App Router) and Tailwind v4, the setup where globals.css starts with @import "tailwindcss".

1. Swap the gradient for three colours (2 minutes)

Delete the bg-gradient-to-r from-purple-500 to-blue-500 (or whatever your hero has). In Tailwind v4 the palette lives in CSS, and every variable in @theme becomes a utility class:

/* app/globals.css */
@theme {
  --color-ink: #0a0a0d;
  --color-paper: #f3f1ea;
  --color-volt: #c6ff3d;
}
Enter fullscreen mode Exit fullscreen mode

Now bg-ink, text-paper and bg-volt exist. Ink is the page, paper is the text (swap them for a light theme), and the accent goes on one or two things per screen: the main button, a status dot. Once the accent is on every element it does the gradient's job again.

2. Two fonts with some character (1 minute)

Pick a display face for headings and UI and a mono for numbers and small labels. The starter uses Space Grotesk and JetBrains Mono. The font @import has to be the first line of the file:

@import url("https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Space+Grotesk:wght@400;500;600;700&display=swap");
@import "tailwindcss";

@theme {
  --font-sans: "Space Grotesk", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "JetBrains Mono", ui-monospace, monospace;
}
Enter fullscreen mode Exit fullscreen mode

One trap I hit while testing this: a project set up with shadcn init (v0 projects use shadcn) already has an @theme inline block with --font-sans: var(--font-sans). That block wins, the variable points at itself, and the page falls back to Times New Roman. Edit the --font-sans and --font-mono lines inside that existing block instead of adding new ones. Also delete any font-family: Arial left on body by the default template, and put font-sans on <body>.

Then use contrast in size and weight: a bold 7xl headline with tracking-tight leading-[1.02], body text at text-lg and 60% opacity, mono at text-xs for the small stuff.

3. Move the hero off centre (1 minute)

Centred layouts are the default because they never look broken. A left-aligned hero with a width limit on the headline looks like someone placed it:

<section className="mx-auto max-w-6xl px-5 pt-28 pb-16">
  <h1 className="max-w-3xl text-5xl leading-[1.02] font-bold tracking-tight sm:text-7xl">
    Know your servers are down before your users do.
  </h1>
  <p className="mt-6 max-w-xl text-lg text-white/60">
    Uptime, latency and deploys on one screen. Set up in two minutes, no agent to install.
  </p>
</section>
Enter fullscreen mode Exit fullscreen mode

Write the headline as one sentence about the visitor's problem. "Supercharge your workflow" is the copy version of the purple gradient.

4. Show the product (2 minutes)

Generated heroes get a blob or a 3D shape because the model has never seen your app. You have. Screenshot your best screen at 2x and put it in a plain browser frame:

import Image from 'next/image';
import shot from '@/assets/dashboard.webp';

<div className="mt-16 overflow-hidden rounded-2xl border border-white/10 bg-ink">
  <div className="flex items-center gap-1.5 border-b border-white/10 px-4 py-3">
    <span className="size-2.5 rounded-full bg-white/15" />
    <span className="size-2.5 rounded-full bg-white/15" />
    <span className="size-2.5 rounded-full bg-white/15" />
  </div>
  <Image src={shot} alt="The dashboard" priority sizes="(max-width: 1200px) 100vw, 1152px" />
</div>
Enter fullscreen mode Exit fullscreen mode

The static import gives next/image the width and height, so nothing shifts when it loads, and it keeps working if you deploy under a sub-path. priority is there because it's the biggest thing above the fold.

No app to screenshot yet? The starter has a /dashboard route you can use as a placeholder. Swap in your real screen before you launch.

5. Make every button respond (3 minutes)

People feel this one without being able to name it. Click "Start free" on most generated pages and nothing visible happens. A loading state that turns into a confirmation tells the visitor their click did something.

I use the free Toast Stack component for this. It installs from a registry URL with the shadcn CLI, so the source lands in your project and you can edit it:

npx shadcn@latest add https://teodoravadani.github.io/qodesign-free/r/toast-stack.json
Enter fullscreen mode Exit fullscreen mode

Mount the toaster once in app/layout.tsx:

import { Toaster } from '@/components/toast-stack/ToastStack';

// inside <body>
{children}
<Toaster />
Enter fullscreen mode Exit fullscreen mode

Then wrap your signup call:

'use client';
import { toast } from '@/components/toast-stack/ToastStack';

const start = () => {
  toast.promise(createWorkspace(), {
    loading: { title: 'Creating your workspace…' },
    success: (name) => ({ title: 'Workspace ready', description: `${name} is live. The first check runs in 30 s.` }),
    error: () => ({ title: 'That didn’t work', description: 'Try again in a minute.' }),
  });
};
Enter fullscreen mode Exit fullscreen mode

The spinner stays up for at least 600 ms so a fast request doesn't flash, then turns into a check. Hover the stack and the toasts fan out.

Pricing gets the same treatment. The Pricing Switch component replaces a static three-column table with a monthly/yearly switch whose prices roll like an odometer:

npx shadcn@latest add https://teodoravadani.github.io/qodesign-free/r/pricing-switch.json
Enter fullscreen mode Exit fullscreen mode
<PricingSwitch
  plans={plans}
  header={<h2>Pay for monitors, not seats.</h2>}
  onSelect={(plan, yearly) => startCheckout(plan.id, yearly ? 'year' : 'month')}
/>
Enter fullscreen mode Exit fullscreen mode

Its digits use Saira Condensed, so add &family=Saira+Condensed:wght@700;800 to the font import from step 2.

Bonus: motion that respects people (30 seconds)

If you add scroll-in animations, put them behind prefers-reduced-motion. In the starter the sections fade up with a CSS scroll timeline and no JavaScript:

@media (prefers-reduced-motion: no-preference) {
  .reveal {
    animation: reveal-up linear both;
    animation-timeline: view();
    animation-range: entry 0% cover 28%;
  }
}
@keyframes reveal-up {
  from { opacity: 0; transform: translateY(28px); }
  to { opacity: 1; transform: none; }
}
Enter fullscreen mode Exit fullscreen mode

Add className="reveal" to a section. The components in the starter check the same setting on their own.

If you'd rather hand it to Cursor

This prompt covers steps 1 to 4. I haven't compared how each tool handles it, so read the diff before you accept it:

Restyle the landing page in this Next.js App Router + Tailwind v4 project.
1. Remove every gradient background. In globals.css add --color-ink #0a0a0d, --color-paper #f3f1ea and --color-volt #c6ff3d to @theme. Use volt only on the primary button and one status dot.
2. Fonts: Space Grotesk for headings and UI, JetBrains Mono for numbers and small labels. Add a Google Fonts @import as the first line of globals.css. If an @theme inline block already defines --font-sans or --font-mono, change those lines instead of adding new ones. Put font-sans on <body>.
3. Left-align the hero: one-sentence headline (max-w-3xl, text-5xl sm:text-7xl, tracking-tight), one supporting line (max-w-xl).
4. Replace the hero illustration with next/image of src/assets/screenshot.webp (static import) inside a simple browser frame.
Don't change any other copy. Don't add dependencies.
Enter fullscreen mode Exit fullscreen mode

What's in the starter

  • /: hero with a product shot, a log upload section with progress and retry (Upload Dropzone), pricing (Pricing Switch), toasts on every action (Toast Stack), and a light/dark switch (Day/Night Toggle).
  • /dashboard: Orbit, an observability dashboard with simulated live data.
  • Next.js 15.5, React 19, TypeScript, Tailwind v4, Framer Motion 11. next build passes and every page is static, so it deploys anywhere. The README has a one-click Vercel button.

About me, and the links. I'm Teodor, and I make animated UI components at qodesign, so the components in this post are mine. Everything here is free and MIT: the starter and the free components and dashboards in qodesign-free. The paid components are $3 each or $29 for all of them, each with a React + Tailwind version and a PROMPT.md for AI coding tools, at qodesign.dev.

If you run the five fixes on your own app, I'd like to see the before and after.

I wrote this post with help from an AI assistant. The code is copied from the starter repo, and every command above was run in a fresh Next.js 15 app (one plain, one set up with shadcn init) with next build passing before publishing.

Top comments (0)