Make a Lovable App Look Less Generic: Swap In 3 Components

7 min read

TL;DR Clone your Lovable project through GitHub sync, install new components with npx shadcn add <registry url>, run tsc as well as the build, and push. New Lovable projects render on the server, so never read localStorage during render. Add a project knowledge note so later prompts keep the swap.

On this page
  1. What changed under new Lovable projects
  2. Why route the swap through GitHub
  3. Install three components from a registry URL
  4. Fix the first type error before you push
  5. Swap 1: a theme toggle that survives server rendering
  6. Swap 2: replace the pricing tabs
  7. Swap 3: toasts with a promise state
  8. Push, then tell Lovable to keep them
  9. A short checklist
  10. FAQ
  11. How do I make my Lovable app look less generic?
  12. Can I add shadcn components to Lovable on the free plan?
  13. Why does my theme toggle break after Lovable moved to TanStack Start?
  14. Will Lovable overwrite components I add through GitHub?

A Lovable app looks generic for the same reason most AI-built apps do: the theme toggle, the pricing tabs and the toasts are the stock pieces every other app ships with. The quickest fix I've found is to swap three of those pieces for components with a bit of character, install them through GitHub sync with the shadcn CLI, and add one line of project knowledge so Lovable keeps them. On a new Lovable project there are two traps on the way, both caused by server-side rendering, and both covered below.

I make qodesign, so the three components here are my free ones: the Day/Night Toggle, Pricing Switch and Toast Stack from the MIT-licensed qodesign-free registry. The steps work the same for any shadcn registry component. I tested everything in a fresh TanStack Start app with Tailwind v4 and shadcn/ui 4.22, ran strict tsc and vite build, and clicked through the built app in headless Chromium. I didn't run the Lovable side of the round trip in this test, so the Lovable steps below come from Lovable's own docs, linked where they appear.

What changed under new Lovable projects

Lovable's docs say TanStack Start became the default for new projects on May 13, 2026. Lovable's post about the switch explains the reason: pages now render on the server, so crawlers and link previews get real HTML instead of an empty shell. Older projects stay on React + Vite unless you upgrade them.

That matters for this job. Any component that touches window or localStorage while it renders now runs on a server first, where neither exists. Plenty of copy-paste theme toggles do exactly that.

Day / Night Toggle running live. A theme switch with a sun that sets into a starry night.

Why route the swap through GitHub

You can paste a component into the chat and ask Lovable to add it. That costs build credits, and the free plan gives you 5 a day, up to 30 a month. The built-in code editor is read-only on the free plan, so you can't paste files in yourself either.

GitHub sync is on every plan for github.com. Per Lovable's Git sync docs, linking a project creates a repository, and commits you push to the synced branch show up in your Lovable project. So the plan is: connect GitHub, clone, run the shadcn CLI locally, check the build, push.

bash
git clone https://github.com/you/your-lovable-app.git
cd your-lovable-app
npm install

If the repo has a components.json at the root, it's set up for shadcn/ui and the commands below work as written. If it doesn't, run npx shadcn@latest init first.

Install three components from a registry URL

bash
npx shadcn@latest add https://teodoravadani.github.io/qodesign-free/r/day-night-toggle.json https://teodoravadani.github.io/qodesign-free/r/pricing-switch.json https://teodoravadani.github.io/qodesign-free/r/toast-stack.json

The CLI wrote five files into src/components and added framer-motion to package.json:

text
src/components/day-night-toggle/DayNightToggle.tsx
src/components/pricing-switch/PricingSwitch.tsx
src/components/pricing-switch/pricing-switch.css
src/components/toast-stack/ToastStack.tsx
src/components/toast-stack/toast-stack.css

Those files are now yours. Nothing auto-updates them, and Lovable can read them like any other file in the project. If you want to look at a registry item before installing it, open its JSON URL in the browser. My post on installing from third-party registries has a checklist for that.

Fix the first type error before you push

Strict tsc failed on the copies I installed on October 21, 2026. If your copies already use import type for ReactNode, skip this step.

text
src/components/pricing-switch/PricingSwitch.tsx(4,3): error TS1484: 'ReactNode' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.
src/components/toast-stack/ToastStack.tsx(6,3): error TS1484: 'ReactNode' is a type and must be imported using a type-only import when 'verbatimModuleSyntax' is enabled.

The TanStack Start template turns on verbatimModuleSyntax, which wants types imported as types. vite build passed anyway, because it strips types without checking them, so this only shows up when you run tsc. The fix is one word in each file:

tsx
import React, {
  type ReactNode,

After that change, tsc --noEmit exited with 0 errors.

Swap 1: a theme toggle that survives server rendering

This is the version a quick prompt tends to produce:

tsx
// the version a chat prompt often produces: reads localStorage during render
const [night, setNight] = useState(() => localStorage.getItem("theme") === "dark")

On a server-rendered route that line throws. In my test the server logged ReferenceError: localStorage is not defined and the browser got a page with nothing in it. The fix splits the job in two: a tiny inline script sets the dark class before the page paints, and the component reads that class after it mounts.

src/components/theme-toggle.tsx:

tsx
import { useEffect, useState } from "react"
import DayNightToggle from "@/components/day-night-toggle/DayNightToggle"

export function ThemeToggle() {
  // false on the server and on the first client render, so the markup matches
  const [night, setNight] = useState(false)

  useEffect(() => {
    // the inline script in __root.tsx already set the class before paint
    setNight(document.documentElement.classList.contains("dark"))
  }, [])

  return (
    <DayNightToggle
      width={72}
      night={night}
      label="Dark mode"
      onChange={(next) => {
        setNight(next)
        document.documentElement.classList.toggle("dark", next)
        try {
          localStorage.setItem("theme", next ? "dark" : "light")
        } catch {
          // private mode or storage blocked: the toggle still works for this visit
        }
      }}
    />
  )
}

Then in src/routes/__root.tsx, add the inline script to head, put suppressHydrationWarning on <html> (the script changes its class before React hydrates), and mount the toaster for swap 3:

tsx
export const Route = createRootRoute({
  head: () => ({
    // ...meta and links as generated
    scripts: [
      {
        // runs before the body paints, so a returning dark-mode visitor never sees a white flash
        children: `try{if(localStorage.getItem("theme")==="dark")document.documentElement.classList.add("dark")}catch(e){}`,
      },
    ],
    // ...
  }),
  // ...
})

function RootDocument({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" suppressHydrationWarning>
      <head>
        <HeadContent />
      </head>
      <body>
        {children}
        <Toaster />
        <Scripts />
      </body>
    </html>
  )
}

What I checked in the built app: the toggle starts unchecked on the server and in the first client render, so React logged no hydration warnings. Clicking it set aria-checked="true", added the dark class and stored theme=dark. After a reload the dark class was already on <html> when the DOM finished loading, before React ran, so there was no white flash. Space toggles it from the keyboard.

One thing I found while tabbing through the page: the first copy of the toggle I installed had no visible focus ring. Its inset shadow is an inline style, and an inline box-shadow beats Tailwind's ring classes, which are box-shadows too. I fixed the registry the same day, and a fresh install now draws keyboard focus as a purple outline that scales with width. If tabbing to your copy shows no ring, run the add command above again. If you add any animated component with a custom shadow, tab to it once and look.

For the same flash problem in a Next.js app, see fixing the dark mode flash with Tailwind. The toggle has its own page at /react/day-night-theme-toggle.

Swap 2: replace the pricing tabs

The stock pricing section is usually a Tabs component with "Monthly" and "Yearly" and three cards that change a number. Pricing Switch keeps the same idea and adds a pill that stretches between the two options, prices that roll like an odometer and a highlighted featured plan. It takes a plain array of plans, so the data stays yours.

src/routes/index.tsx:

tsx
import { createFileRoute } from "@tanstack/react-router"
import { PricingSwitch, type Plan } from "@/components/pricing-switch/PricingSwitch"
import { toast } from "@/components/toast-stack/ToastStack"
import { ThemeToggle } from "@/components/theme-toggle"

export const Route = createFileRoute("/")({ component: Home })

const plans: Plan[] = [
  {
    id: "starter",
    name: "Starter",
    description: "For a side project that just launched.",
    monthly: 9,
    yearly: 7,
    cta: "Start free",
    features: ["1 project", "Email support"],
  },
  {
    id: "pro",
    name: "Pro",
    description: "For apps with paying users.",
    monthly: 29,
    yearly: 24,
    cta: "Go Pro",
    features: ["10 projects", "Priority support", "Custom domain"],
    featured: true,
  },
  {
    id: "team",
    name: "Team",
    note: "up to 5 seats",
    description: "For a small team shipping together.",
    monthly: 79,
    yearly: 66,
    cta: "Talk to us",
    features: ["Unlimited projects", "Shared billing"],
  },
]

function Home() {
  return (
    <main className="min-h-svh bg-background text-foreground">
      <header className="flex items-center justify-between p-6">
        <span className="font-medium">Acme</span>
        <ThemeToggle />
      </header>
      <PricingSwitch
        plans={plans}
        header={<h1 className="text-3xl font-semibold">Pricing</h1>}
        onSelect={(plan, yearly) => {
          toast.promise(
            new Promise((resolve) => setTimeout(resolve, 900)),
            {
              loading: { title: `Starting ${plan.name}` },
              success: () => ({
                title: `${plan.name} selected`,
                description: yearly ? "Billed yearly" : "Billed monthly",
              }),
            },
          )
        }}
      />
    </main>
  )
}

In the built app, the Monthly/Yearly control is a pair of radio buttons with aria-checked. Choosing Yearly changed the written-out price on each card to "$7 per month, $84 billed yearly", "$24 per month, $288 billed yearly" and "$66 per month, $792 billed yearly", so the price is in the page text as well as in the rolling digits. At 390 px wide the page had no sideways scroll. More on the component at /react/pricing-toggle-monthly-yearly.

Swap 3: toasts with a promise state

<Toaster /> sits in the root layout from swap 1, and toast is a plain function you can call from anywhere. In the pricing example, pressing "Go Pro" shows a spinner toast ("Starting Pro") that turns into a check with "Pro selected / Billed yearly" when the promise settles. Both texts appeared in the built app, in that order. If your Lovable app already uses another toast library, remove its <Toaster /> from the root when you add this one, so you don't end up with two stacks. Details are on /react/toast-stack. If you build in Next.js instead and the toast has to follow a server action, see Toast Notifications After a Next.js Server Action.

Push, then tell Lovable to keep them

bash
npx tsc --noEmit
npm run build
git add -A
git commit -m "Swap theme toggle, pricing and toasts"
git push

According to Lovable's docs, the commit then appears in your project, and synced commits that change dependencies go through Lovable's dependency audit. The sync card in Project settings → Git tells you whether Lovable and GitHub are on the same commit.

The last step stops the next prompt from undoing your work. Project knowledge is a set of standing instructions for Lovable's agent. The knowledge docs say it lives in Project settings → Knowledge, with room for 10,000 characters. Something like this is enough:

text
UI components:
- Theme switching uses src/components/theme-toggle.tsx (wraps DayNightToggle). Don't replace it with a Switch.
- Pricing uses PricingSwitch from src/components/pricing-switch. Add plans to the plans array instead of building new cards.
- Notifications use toast() and <Toaster /> from src/components/toast-stack. Don't add another toast library.
- Never read window or localStorage during render. Do it in useEffect.

The last rule is worth keeping even if you skip the rest, because it's the server-rendering trap from swap 1 in one line.

A short checklist

  • Clone through GitHub sync and run npx shadcn@latest add <registry url> locally.
  • Run tsc --noEmit as well as the build. The build alone missed the type errors.
  • Keep localStorage and window out of render. Set the theme class with an inline head script.
  • Tab through every new component and check that focus is visible.
  • Remove the old component's provider (a second <Toaster /> is the common one).
  • Add a project knowledge note so Lovable keeps the swap.

If you're picking where components come from in the first place, choosing a React + Tailwind component library compares the options, and qodesign for Lovable covers how the paid pieces fit a Lovable project. They're React + TypeScript with Tailwind (most also use Framer Motion). For v0 apps the same idea is in making a v0 app not look AI-generated.

FAQ

How do I make my Lovable app look less generic?

Replace the stock pieces people see first, usually the theme toggle, the pricing section and the notifications, with components that have their own motion and layout. Install them through GitHub sync with npx shadcn@latest add <registry url>, then add a project knowledge note so later prompts keep them.

Can I add shadcn components to Lovable on the free plan?

Yes, through GitHub. Lovable's code editor is read-only on the free plan, but GitHub sync for github.com is on every plan. Clone the repo, run the shadcn CLI locally and push. Lovable's docs say commits pushed to the synced branch appear in the project.

Why does my theme toggle break after Lovable moved to TanStack Start?

New Lovable projects render on the server. Code that reads localStorage or window during render runs where neither exists and throws localStorage is not defined. Read storage in useEffect, and set the initial theme class with a small inline script in the document head.

Will Lovable overwrite components I add through GitHub?

Synced commits become part of the project, and Lovable edits them like any other file when a prompt asks it to. To keep them, say so in Project settings → Knowledge, for example "Notifications use toast() from src/components/toast-stack. Don't add another toast library."