Dark Mode

Add dark mode and a Light / Dark / System mode toggle to your Expo app — for NativeWind and Uniwind.

lvcn themes ship with full light and dark token sets. This guide adds a persistent, shadcn-style theme provider and a mode toggle (Light / Dark / System) to your app. Choose your style engine below.

Projects scaffolded with lovda init already include light and dark CSS variables and the dark mode wiring. In that case you only need the provider and toggle from the steps below.

Enable class-based dark mode.

NativeWind switches the dark: variant from a class, so set darkMode: "class" in your Tailwind config:

tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  darkMode: "class",
  content: ["./src/**/*.{js,jsx,ts,tsx}"],
  presets: [require("nativewind/preset")],
  // ...
};

Then make sure global.css defines your tokens for light (:root) and dark (.dark:root). NativeWind uses HSL values:

global.css
:root {
  --background: 0 0% 100%;
  --foreground: 0 0% 3.9%;
  /* ...remaining tokens... */
}

.dark:root {
  --background: 0 0% 3.9%;
  --foreground: 0 0% 98%;
  /* ...remaining tokens... */
}

See Theming for the complete token list.

Install the persistence dependency.

The chosen theme is stored so it survives app restarts.

npx expo install @react-native-async-storage/async-storage

Create a theme provider.

components/theme-provider.tsx
import AsyncStorage from "@react-native-async-storage/async-storage"
import * as React from "react"
import { useColorScheme } from "nativewind"

type Theme = "light" | "dark" | "system"

type ThemeProviderState = {
  theme: Theme
  setTheme: (theme: Theme) => void
}

const ThemeProviderContext = React.createContext<ThemeProviderState>({
  theme: "system",
  setTheme: () => null,
})

export function ThemeProvider({
  children,
  defaultTheme = "system",
  storageKey = "lvcn-ui-theme",
}: {
  children: React.ReactNode
  defaultTheme?: Theme
  storageKey?: string
}) {
  const { setColorScheme } = useColorScheme()
  const [theme, setThemeState] = React.useState<Theme>(defaultTheme)

  // Restore the saved preference on launch.
  React.useEffect(() => {
    AsyncStorage.getItem(storageKey).then((value) => {
      const next = (value as Theme | null) ?? defaultTheme
      setThemeState(next)
      setColorScheme(next)
    })
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [])

  const value = React.useMemo(
    () => ({
      theme,
      setTheme: (next: Theme) => {
        setThemeState(next)
        setColorScheme(next) // drives NativeWind's `dark:` variant
        AsyncStorage.setItem(storageKey, next)
      },
    }),
    [theme, setColorScheme, storageKey]
  )

  return (
    <ThemeProviderContext.Provider value={value}>
      {children}
    </ThemeProviderContext.Provider>
  )
}

export function useTheme() {
  return React.useContext(ThemeProviderContext)
}

Wrap your root layout.

app/_layout.tsx
import "../global.css"

import { Stack } from "expo-router"

import { ThemeProvider } from "@/components/theme-provider"

export default function RootLayout() {
  return (
    <ThemeProvider defaultTheme="system">
      <Stack screenOptions={{ headerShown: false }} />
    </ThemeProvider>
  )
}

Add a mode toggle.

The toggle reuses lvcn's own components. Add them first:

npx lovdacn@latest add button dropdown-menu icon text
components/mode-toggle.tsx
import { Moon, Sun } from "lucide-react-native"

import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { Icon } from "@/components/ui/icon"
import { Text } from "@/components/ui/text"
import { useTheme } from "@/components/theme-provider"

export function ModeToggle() {
  const { setTheme } = useTheme()

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          <Icon
            as={Sun}
            className="size-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0"
          />
          <Icon
            as={Moon}
            className="absolute size-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100"
          />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuItem onPress={() => setTheme("light")}>
          <Text>Light</Text>
        </DropdownMenuItem>
        <DropdownMenuItem onPress={() => setTheme("dark")}>
          <Text>Dark</Text>
        </DropdownMenuItem>
        <DropdownMenuItem onPress={() => setTheme("system")}>
          <Text>System</Text>
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

Use it.

Drop <ModeToggle /> anywhere inside the provider — a screen header, tab bar, or settings screen:

import { ModeToggle } from "@/components/mode-toggle"

<ModeToggle />

How it works

  • System follows the device appearance and keeps tracking it as the user changes their OS setting.
  • Light / Dark override the device and lock the app to that appearance. On NativeWind, setColorScheme toggles the dark class that powers dark: utilities; on Uniwind, Uniwind.setTheme switches the active theme.
  • The selection is persisted with AsyncStorage and re-applied on the next launch, so the toggle behaves like shadcn/ui on the web.

Use semantic tokens

For components to adapt automatically, style them with semantic classes instead of hard-coded colors:

import { View } from "react-native"
import { Text } from "@/components/ui/text"

export function Panel() {
  return (
    <View className="bg-background border-border rounded-lg border p-4">
      <Text className="text-foreground">Adapts to light and dark</Text>
    </View>
  )
}

This documentation site

For reference, this site (Next.js) uses next-themes with the class strategy — use the sun / moon control in the header to preview tokens:

<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
  {children}
</ThemeProvider>

Checklist

  1. Light and dark values exist for every token
  2. The root layout is wrapped in ThemeProvider
  3. Components use semantic classes (bg-background, text-foreground, border-border)