This guide covers how to set up lovdaCN in an existing Expo project without using the init command.
Choose and set up your style engine
lovdaCN supports both NativeWind and Uniwind. Select one and follow its configuration:
- Follow the official NativeWind v4 installation guide to add NativeWind to your Expo project.
- Update your
metro.config.jsto configure the Tailwind output withinlineRem: 16to ensure typography scales properly:
const { getDefaultConfig } = require("expo/metro-config")
const { withNativeWind } = require("nativewind/metro")
const config = getDefaultConfig(__dirname)
module.exports = withNativeWind(config, {
input: "./global.css",
inlineRem: 16,
})Install dependencies
Install the core styling utilities, icons, and the React Native primitives that power the interactive components (overlays, gestures, and asChild):
npm install class-variance-authority clsx tailwind-merge lucide-react-native react-native-reanimated react-native-gesture-handler @rn-primitives/portal @rn-primitives/slotConfigure path aliases
Ensure your project is set up to resolve the @/* path alias in tsconfig.json:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}Configure CSS variables and styling
Create a global.css file in your project root (or src/global.css) and add the theme variables:
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--radius: 0.5rem;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
}
}Also configure your tailwind.config.js to map these variables:
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"],
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
},
},
plugins: [],
}Add the cn helper
Create a utility file to merge classNames together:
import { clsx, type ClassValue } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}Set up the root layout
Import your global CSS at the top of your app entry point, then wrap the app in GestureHandlerRootView and mount a <PortalHost /> at the root. Overlay components — Dialog, AlertDialog, Popover, Tooltip, DropdownMenu, ContextMenu, HoverCard, Select, and Menubar — render through this portal and will not work without it:
import "../global.css"
import { Stack } from "expo-router"
import { GestureHandlerRootView } from "react-native-gesture-handler"
import { PortalHost } from "@rn-primitives/portal"
export default function RootLayout() {
return (
<GestureHandlerRootView style={{ flex: 1 }}>
<Stack />
<PortalHost />
</GestureHandlerRootView>
)
}Create lvcn.json file
Create a configuration file in your project root:
{
"$schema": "https://lovdacn.vercel.app/schema.json",
"style": "nova",
"styleEngine": "nativewind",
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "./global.css",
"baseColor": "neutral"
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui"
},
"components": []
}Adjust the config properties according to your setup (e.g. set styleEngine to uniwind and omit tailwind.config if you use Uniwind).
All set!
You can now start adding components to your project. Use the CLI to pull components into your workspace — each component's @rn-primitives/* primitive is installed automatically as you add it:
npx lovdacn@latest add button card(Note: You can also use the shortened lvcn alias, e.g., npx lvcn@latest add button card.)