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.
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.
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.
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.
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.
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.
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.
This is the version a quick prompt tends to produce:
tsx
// the version a chat prompt often produces: reads localStorage during renderconst [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.
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.
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.
<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.
npx tsc --noEmitnpm run buildgit add -Agit 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.
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.
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.
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.
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."