lvcn is designed to work seamlessly with Expo Router, the file-based routing system for Expo apps. This guide covers setup and best practices.
New project
When you scaffold a new project with the CLI, Expo Router is included and configured automatically:
npx lovdacn@latest init -n my-app
# or
npx lvcn@latest init -n my-appThe generated template includes:
app/directory with file-based routingapp/_layout.tsx— root layout with theme provider and font loadingapp/(tabs)/— tab navigation groupapp/(tabs)/_layout.tsx— tab bar configuration
Existing project
If you already have an Expo Router project, run init to add lvcn:
npx lovdacn@latest init
# or
npx lvcn@latest initThen add your first component:
npx lovdacn@latest add button text
# or
npx lvcn@latest add button textRoot layout setup
Your root layout needs to:
- Load fonts (if using custom fonts)
- Import global CSS
- Wrap the app in any required providers
// app/_layout.tsx
import "../global.css"
import { Stack } from "expo-router"
import { useFonts } from "expo-font"
import { StatusBar } from "expo-status-bar"
export default function RootLayout() {
const [loaded] = useFonts({
Inter: require("../assets/fonts/Inter.ttf"),
})
if (!loaded) return null
return (
<>
<StatusBar style="auto" />
<Stack
screenOptions={{
headerShown: false,
}}
/>
</>
)
}Tab navigation with lvcn
Use lvcn components inside your tab screens:
// app/(tabs)/index.tsx
import { View } from "react-native"
import { Text } from "@/components/ui/text"
import { Button } from "@/components/ui/button"
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
export default function HomeScreen() {
return (
<View className="flex-1 items-center justify-center gap-4 p-6">
<Text variant="h1">Welcome</Text>
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>
<Text>Get Started</Text>
</CardTitle>
</CardHeader>
<CardContent>
<Button>
<Text>Press me</Text>
</Button>
</CardContent>
</Card>
</View>
)
}Modal screens
Use Dialog or AlertDialog components within Expo Router modal routes:
// app/modal.tsx
import { View } from "react-native"
import { router } from "expo-router"
import { Text } from "@/components/ui/text"
import { Button } from "@/components/ui/button"
export default function ModalScreen() {
return (
<View className="flex-1 items-center justify-center gap-4 p-6">
<Text variant="h2">Modal</Text>
<Button onPress={() => router.back()}>
<Text>Close</Text>
</Button>
</View>
)
}Configure the modal in your layout:
// app/_layout.tsx
<Stack>
<Stack.Screen name="(tabs)" options={{ headerShown: false }} />
<Stack.Screen name="modal" options={{ presentation: "modal" }} />
</Stack>NativeWind configuration
Ensure your metro.config.js includes NativeWind support:
const { getDefaultConfig } = require("expo/metro-config")
const { withNativeWind } = require("nativewind/metro")
const config = getDefaultConfig(__dirname)
module.exports = withNativeWind(config, {
input: "./src/global.css",
})Uniwind configuration
For Uniwind projects, the metro plugin is configured automatically:
const { getDefaultConfig } = require("expo/metro-config")
const { withUniwwind } = require("uniwind/metro")
const config = getDefaultConfig(__dirname)
module.exports = withUniwwind(config, {
input: "./src/global.css",
})Platform-specific behavior
lvcn components use Platform.select internally where native and web behavior differs. When using Expo Router's web support, components automatically adapt:
- Dialogs use native modal presentations on iOS/Android, DOM portals on web
- Select uses native picker on mobile, HTML select on web
- Popovers use measured positioning on native, CSS positioning on web
You don't need to write platform checks for these — the components handle it.
Related
- Installation — full setup guide
- Dark Mode — theme toggling with Expo
- Components — browse all components