AniUI

Theme Provider

Theme context provider with light, dark, and system mode support.

Installation#

npx @aniui/cli add theme-provider
LightDark
Light mode enabled
Web preview — components render natively on iOS & Android
import { ThemeProvider } from "@/components/ui/theme-provider";

export default function App() {
  return (
    <ThemeProvider defaultTheme="system">
      {/* Your app content */}
    </ThemeProvider>
  );
}

Usage#

app/_layout.tsx
import { ThemeProvider } from "@/components/ui/theme-provider";

export default function App() {
  return (
    <ThemeProvider defaultTheme="system">
      {/* Your app content */}
    </ThemeProvider>
  );
}

useTheme Hook#

Access the current theme, resolved theme, and theme controls from any child component.

Using useTheme
import { useTheme } from "@/components/ui/theme-provider";

export function ThemeToggle() {
  const { theme, resolvedTheme, setTheme, toggleTheme } = useTheme();

  return (
    <Button onPress={toggleTheme}>
      {resolvedTheme === "dark" ? "Switch to Light" : "Switch to Dark"}
    </Button>
  );
}
PropTypeDefault
theme
"light" | "dark" | "system"
-
resolvedTheme
"light" | "dark"
-
setTheme
(theme: Theme) => void
-
toggleTheme
() => void
-

useThemeColors Hook#

Native props and Skia canvases can't read className tokens (a Switch's trackColor, a TextInput's caret, an icon's color, a chart's canvas fill). useThemeColors() is the single source of truth those read from instead — a resolved hex palette matching your project's current theme preset and light/dark mode, generated by aniui init and kept in sync whenever you run aniui theme. Every AniUI component that needs a literal color value already uses it — reach for it yourself when a component you write needs the same.

Using useThemeColors
import { useThemeColors } from "@/components/ui/theme-provider";

export function CustomSwitch({ value }: { value: boolean }) {
  const colors = useThemeColors();

  return (
    <Switch
      value={value}
      trackColor={{ false: colors.border, true: colors.primary }}
      thumbColor={colors.background}
    />
  );
}
PropTypeDefault
background
string (hex)
-
foreground
string (hex)
-
card / cardForeground
string (hex)
-
primary / primaryForeground
string (hex)
-
secondary / secondaryForeground
string (hex)
-
muted / mutedForeground
string (hex)
-
accent / accentForeground
string (hex)
-
destructive / destructiveForeground
string (hex)
-
border / input / ring
string (hex)
-

Props#

PropTypeDefault
children
React.ReactNode
-
defaultTheme
"light" | "dark" | "system"
"system"
className
string
-

Accessibility#

  • ThemeProvider context for light/dark/system mode.
  • Respects the user's system-level accessibility preferences for color scheme.

Source#

components/ui/theme-provider.tsx
import React, { createContext, useCallback, useContext, useEffect, useState } from "react";
import { Appearance, useColorScheme as useNativeColorScheme, View } from "react-native";
import { cn } from "@/lib/utils";

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

const ThemeContext = createContext<{
  theme: Theme;
  resolvedTheme: "light" | "dark";
  setTheme: (theme: Theme) => void;
  toggleTheme: () => void;
}>({
  theme: "system",
  resolvedTheme: "light",
  setTheme: () => {},
  toggleTheme: () => {},
});

export function useTheme() {
  return useContext(ThemeContext);
}

export interface ThemeProviderProps {
  children: React.ReactNode;
  defaultTheme?: Theme;
  className?: string;
}

// Try to detect and use Uniwind's setTheme if available
let uniwindSetTheme: ((theme: string) => void) | null = null;
try {
  const mod = require("uniwind");
  if (mod?.Uniwind?.setTheme) {
    uniwindSetTheme = (theme: string) => mod.Uniwind.setTheme(theme);
  }
} catch {}

export function ThemeProvider({
  children,
  defaultTheme = "system",
  className,
}: ThemeProviderProps) {
  const systemScheme = useNativeColorScheme();
  const [theme, setThemeState] = useState<Theme>(defaultTheme);

  const resolvedTheme: "light" | "dark" =
    theme === "system" ? (systemScheme === "dark" ? "dark" : "light") : theme;

  const applyTheme = useCallback((resolved: "light" | "dark") => {
    if (uniwindSetTheme) {
      // Uniwind handles Appearance.setColorScheme internally
      uniwindSetTheme(resolved);
    } else {
      // NativeWind v4/v5: set system appearance directly
      try { Appearance.setColorScheme(resolved); } catch {}
    }
  }, []);

  const setTheme = useCallback((newTheme: Theme) => {
    setThemeState(newTheme);
    const resolved = newTheme === "system" ? (systemScheme === "dark" ? "dark" : "light") : newTheme;
    applyTheme(resolved);
  }, [systemScheme, applyTheme]);

  const toggleTheme = useCallback(() => {
    setThemeState((prev) => {
      const current = prev === "system" ? (systemScheme === "dark" ? "dark" : "light") : prev;
      const next = current === "dark" ? "light" : "dark";
      applyTheme(next);
      return next;
    });
  }, [systemScheme, applyTheme]);

  // Apply initial theme
  useEffect(() => {
    if (defaultTheme !== "system") {
      applyTheme(defaultTheme === "dark" ? "dark" : "light");
    }
  }, [defaultTheme, applyTheme]);

  return (
    <ThemeContext.Provider value={{ theme, resolvedTheme, setTheme, toggleTheme }}>
      <View className={cn(resolvedTheme === "dark" ? "dark" : "", "flex-1 bg-background", className)}>
        {children}
      </View>
    </ThemeContext.Provider>
  );
}

// ─── AUTO-GENERATED THEME COLORS (written by `aniui init` / `aniui theme`) ───
// Do not hand-edit values below — regenerated whenever the theme preset changes.
export const THEME_COLORS = {
  light: { background: "#ffffff", foreground: "#09090b", card: "#ffffff", cardForeground: "#09090b", primary: "#18181b", primaryForeground: "#fafafa", secondary: "#f4f4f5", secondaryForeground: "#18181b", muted: "#f4f4f5", mutedForeground: "#71717a", accent: "#f4f4f5", accentForeground: "#18181b", destructive: "#ef4444", destructiveForeground: "#fafafa", border: "#e4e4e7", input: "#e4e4e7", ring: "#18181b" },
  dark: { background: "#09090b", foreground: "#fafafa", card: "#09090b", cardForeground: "#fafafa", primary: "#fafafa", primaryForeground: "#18181b", secondary: "#27272a", secondaryForeground: "#fafafa", muted: "#27272a", mutedForeground: "#a1a1aa", accent: "#27272a", accentForeground: "#fafafa", destructive: "#7f1d1d", destructiveForeground: "#fafafa", border: "#27272a", input: "#27272a", ring: "#d4d4d8" },
} as const;

/**
 * The one place every component reads colors from when a native prop (a
 * toggle control's track/thumb, TextInput caret, Skia canvas fills, lucide
 * icon `color`, etc.) can't take a className token. Deliberately reads
 * useColorScheme() directly rather than useTheme().resolvedTheme — the
 * latter's context default doesn't track system appearance without a
 * <ThemeProvider> ancestor, and this needs to stay reactive even in apps
 * that never mount one.
 */
export function useThemeColors() {
  const scheme = useNativeColorScheme();
  return scheme === "dark" ? THEME_COLORS.dark : THEME_COLORS.light;
}
// ─── END AUTO-GENERATED THEME COLORS ───