ScaleRocket/Mobile

Theme

Dark mode with useColorScheme, system theme detection, and theme context for consistent styling.

Theme

ScaleRocket Mobile supports light and dark mode out of the box, following the device's system theme by default with the option to let users override.

System Theme Detection

React Native provides useColorScheme to detect the device setting:

import { useColorScheme } from "react-native";

export default function MyScreen() {
  const colorScheme = useColorScheme(); // "light" | "dark" | null

  return (
    <View style={{
      backgroundColor: colorScheme === "dark" ? "#111827" : "#FFFFFF"
    }}>
      <Text style={{ color: colorScheme === "dark" ? "#F9FAFB" : "#111827" }}>
        Hello World
      </Text>
    </View>
  );
}

Theme Context

Instead of calling useColorScheme everywhere, ScaleRocket uses a centralized theme context:

import { createContext, useContext, useState, useEffect } from "react";
import { useColorScheme as useDeviceTheme } from "react-native";
import AsyncStorage from "@react-native-async-storage/async-storage";

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

interface ThemeContextType {
  theme: Theme;
  colorScheme: "light" | "dark";
  setTheme: (theme: Theme) => void;
  colors: typeof lightColors;
}

const ThemeContext = createContext<ThemeContextType | null>(null);

export function ThemeProvider({ children }: { children: React.ReactNode }) {
  const deviceTheme = useDeviceTheme();
  const [theme, setThemeState] = useState<Theme>("system");

  useEffect(() => {
    AsyncStorage.getItem("theme").then((saved) => {
      if (saved) setThemeState(saved as Theme);
    });
  }, []);

  const setTheme = (newTheme: Theme) => {
    setThemeState(newTheme);
    AsyncStorage.setItem("theme", newTheme);
  };

  const colorScheme = theme === "system"
    ? (deviceTheme ?? "light")
    : theme;

  const colors = colorScheme === "dark" ? darkColors : lightColors;

  return (
    <ThemeContext.Provider value={{ theme, colorScheme, setTheme, colors }}>
      {children}
    </ThemeContext.Provider>
  );
}

export const useTheme = () => useContext(ThemeContext)!;

Color Tokens

Define semantic color tokens for both themes:

export const lightColors = {
  background: "#FFFFFF",
  surface: "#F9FAFB",
  text: "#111827",
  textSecondary: "#6B7280",
  primary: "#3B82F6",
  border: "#E5E7EB",
  error: "#EF4444",
  success: "#10B981",
};

export const darkColors = {
  background: "#111827",
  surface: "#1F2937",
  text: "#F9FAFB",
  textSecondary: "#9CA3AF",
  primary: "#60A5FA",
  border: "#374151",
  error: "#F87171",
  success: "#34D399",
};

Using Theme in Components

Access colors from the theme context:

import { useTheme } from "@/lib/theme";

function ProfileCard() {
  const { colors } = useTheme();

  return (
    <View style={{
      backgroundColor: colors.surface,
      borderColor: colors.border,
      borderWidth: 1,
      borderRadius: 12,
      padding: 16,
    }}>
      <Text style={{ color: colors.text, fontSize: 18, fontWeight: "600" }}>
        Profile
      </Text>
      <Text style={{ color: colors.textSecondary, marginTop: 4 }}>
        Edit your information
      </Text>
    </View>
  );
}

Theme Switcher

Let users choose between light, dark, and system:

function ThemeSettings() {
  const { theme, setTheme } = useTheme();

  const options: { label: string; value: Theme }[] = [
    { label: "Light", value: "light" },
    { label: "Dark", value: "dark" },
    { label: "System", value: "system" },
  ];

  return (
    <View style={{ gap: 8 }}>
      {options.map((option) => (
        <TouchableOpacity
          key={option.value}
          onPress={() => setTheme(option.value)}
          style={{
            flexDirection: "row",
            justifyContent: "space-between",
            padding: 16,
          }}
        >
          <Text>{option.label}</Text>
          {theme === option.value && (
            <Ionicons name="checkmark" size={20} color="#3B82F6" />
          )}
        </TouchableOpacity>
      ))}
    </View>
  );
}

Setup in Root Layout

// app/_layout.tsx
import { ThemeProvider } from "@/lib/theme";

export default function RootLayout() {
  return (
    <ThemeProvider>
      <Slot />
    </ThemeProvider>
  );
}

Update the status bar and navigation bar to match:

import { StatusBar } from "expo-status-bar";

function App() {
  const { colorScheme } = useTheme();
  return <StatusBar style={colorScheme === "dark" ? "light" : "dark"} />;
}

On this page