Manual Installation

Step-by-step guide to manually configure lovdaCN in your Expo project.

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:

  1. Follow the official NativeWind v4 installation guide to add NativeWind to your Expo project.
  2. Update your metro.config.js to configure the Tailwind output with inlineRem: 16 to ensure typography scales properly:
metro.config.js
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/slot

Configure path aliases

Ensure your project is set up to resolve the @/* path alias in tsconfig.json:

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:

global.css
@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:

tailwind.config.js
/** @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:

lib/utils.ts
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:

app/_layout.tsx
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:

lvcn.json
{
  "$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.)