◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 27 days left. If your app calls it, check the migration target●SDK 58 — Moves to React Native 0.88. R8 is now on by default in Android release builds, and iOS prebuild generates a SceneDelegate●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-03-14Beginner

Rork Max Dark Mode & Theming Guide — Customizing Your App's Appearance

Complete dark mode, light mode, and custom theming implementation. Design token management and modern UI patterns for Rork Max.

rork-max40themingdark-modedesign-tokensui-design3

Dark Mode & Theming in Rork Max

Dark mode has evolved from a nice-to-have feature to an essential requirement. Users expect apps to respect their system preferences, reduce eye strain during evening use, and conserve battery on OLED displays. Creating a themeable app demonstrates attention to user experience and accessibility.

Organizing Design Tokens

Color Palette and Design System

The foundation of effective theming is centralizing design tokens—colors, spacing, typography, and other design values.

// theme/tokens.js
const lightTheme = {
  colors: {
    primary: '#0066CC',
    secondary: '#FF6B6B',
    background: '#FFFFFF',
    surface: '#F5F5F5',
    text: '#222222',
    textSecondary: '#666666',
    border: '#DDDDDD',
    success: '#22C55E',
    warning: '#FBBF24',
    error: '#EF4444',
  },
  spacing: {
    xs: 4,
    sm: 8,
    md: 16,
    lg: 24,
    xl: 32,
  },
  fontSize: {
    sm: 12,
    base: 14,
    lg: 16,
    xl: 20,
    '2xl': 24,
  },
};
 
const darkTheme = {
  colors: {
    primary: '#4A9EFF',
    secondary: '#FF8A9B',
    background: '#1A1A1A',
    surface: '#2D2D2D',
    text: '#FFFFFF',
    textSecondary: '#CCCCCC',
    border: '#404040',
    success: '#4ADE80',
    warning: '#FACC15',
    error: '#F87171',
  },
  spacing: lightTheme.spacing,
  fontSize: lightTheme.fontSize,
};
 
export { lightTheme, darkTheme };

Centralizing tokens eliminates duplication and makes theme switching effortless.

Managing Global Theme State

Context API for Theme Distribution

React Context allows you to make the current theme accessible throughout your app without prop drilling.

import { createContext, useState, useContext, useEffect } from 'react';
import { useColorScheme } from 'react-native';
import { lightTheme, darkTheme } from './tokens';
 
const ThemeContext = createContext();
 
export function ThemeProvider({ children }) {
  const systemColorScheme = useColorScheme();
  const [isDarkMode, setIsDarkMode] = useState(
    systemColorScheme === 'dark'
  );
 
  // Sync with system preferences
  useEffect(() => {
    if (systemColorScheme) {
      setIsDarkMode(systemColorScheme === 'dark');
      console.log(`System theme detected: ${systemColorScheme}`);
      // Expected output: System theme detected: dark
    }
  }, [systemColorScheme]);
 
  const theme = isDarkMode ? darkTheme : lightTheme;
 
  const toggleTheme = () => {
    setIsDarkMode(!isDarkMode);
    console.log(`Theme toggled to: ${!isDarkMode ? 'dark' : 'light'}`);
    // Expected output: Theme toggled to: dark
  };
 
  const value = {
    isDarkMode,
    theme,
    toggleTheme,
  };
 
  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  );
}
 
export function useTheme() {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error('useTheme must be used within ThemeProvider');
  }
  return context;
}

Applying Themes to Components

Using Theme Values in Components

Components consume theme values from the context hook, automatically updating when the theme changes.

import { useTheme } from './ThemeProvider';
import { View, Text, StyleSheet } from 'react-native';
 
export function ThemedCard({ title, description }) {
  const { theme } = useTheme();
 
  const styles = StyleSheet.create({
    container: {
      backgroundColor: theme.colors.surface,
      borderColor: theme.colors.border,
      borderWidth: 1,
      borderRadius: 8,
      padding: theme.spacing.md,
      marginBottom: theme.spacing.md,
    },
    title: {
      color: theme.colors.text,
      fontSize: theme.fontSize.lg,
      fontWeight: 'bold',
      marginBottom: theme.spacing.sm,
    },
    description: {
      color: theme.colors.textSecondary,
      fontSize: theme.fontSize.base,
      lineHeight: 20,
    },
  });
 
  return (
    <View style={styles.container}>
      <Text style={styles.title}>{title}</Text>
      <Text style={styles.description}>{description}</Text>
    </View>
  );
}

This pattern ensures components always use the correct colors for the current theme.

Persisting User Preferences

Saving Theme Selection

Users expect their theme choice to persist across app sessions. Store preferences locally.

import AsyncStorage from '@react-native-async-storage/async-storage';
import { useEffect } from 'react';
 
const THEME_PREFERENCE_KEY = '@rork_theme_preference';
 
export function usePersistentTheme() {
  const { isDarkMode, toggleTheme } = useTheme();
 
  // Load saved preference on startup
  useEffect(() => {
    loadThemePreference();
  }, []);
 
  // Save whenever theme changes
  useEffect(() => {
    saveThemePreference(isDarkMode);
  }, [isDarkMode]);
 
  const loadThemePreference = async () => {
    try {
      const savedTheme = await AsyncStorage.getItem(THEME_PREFERENCE_KEY);
      if (savedTheme !== null) {
        const isDark = savedTheme === 'dark';
        if (isDark !== isDarkMode) {
          toggleTheme();
        }
        console.log(`Loaded saved theme: ${savedTheme}`);
        // Expected output: Loaded saved theme: dark
      }
    } catch (error) {
      console.error('Theme load error:', error);
    }
  };
 
  const saveThemePreference = async (isDark) => {
    try {
      const themeValue = isDark ? 'dark' : 'light';
      await AsyncStorage.setItem(THEME_PREFERENCE_KEY, themeValue);
    } catch (error) {
      console.error('Theme save error:', error);
    }
  };
}

Dark Mode Best Practices

Accessibility and Contrast

Sufficient color contrast is critical for readability and accessibility compliance.

// Ensure adequate contrast ratios
const theme = {
  colors: {
    // Light mode
    lightText: '#222222',        // Against #FFFFFF: 17:1 ratio (WCAG AAA)
    lightBg: '#FFFFFF',
 
    // Dark mode
    darkText: '#FFFFFF',         // Against #1A1A1A: 16:1 ratio (WCAG AAA)
    darkBg: '#1A1A1A',
  },
};
 
export function AccessibleText({ children, isDark }) {
  const color = isDark ? theme.colors.darkText : theme.colors.lightText;
 
  return (
    <Text style={{ color }}>
      {children}
    </Text>
  );
}

WCAG 2.1 guidelines recommend a minimum 4.5:1 contrast ratio for normal text. Aim for 7:1 when possible.

Building Custom Themes

User-Defined Theme Creation

Enable advanced users to create personalized themes.

import { useState } from 'react';
 
export function useCustomTheme() {
  const [customThemes, setCustomThemes] = useState([]);
 
  const createCustomTheme = (name, colors) => {
    const newTheme = {
      id: Date.now(),
      name,
      colors: {
        primary: colors.primary,
        secondary: colors.secondary,
        background: colors.background,
        surface: colors.surface,
        text: colors.text,
        textSecondary: colors.textSecondary,
      },
    };
 
    setCustomThemes([...customThemes, newTheme]);
    console.log(`Custom theme created: ${name}`);
    // Expected output: Custom theme created: MyCyan
 
    return newTheme;
  };
 
  const deleteCustomTheme = (themeId) => {
    setCustomThemes(customThemes.filter(t => t.id !== themeId));
  };
 
  return {
    customThemes,
    createCustomTheme,
    deleteCustomTheme,
  };
}
 
export function CustomThemeEditor() {
  const [themeName, setThemeName] = useState('');
  const [primaryColor, setPrimaryColor] = useState('#0066CC');
  const { createCustomTheme } = useCustomTheme();
 
  const handleCreate = () => {
    if (themeName) {
      createCustomTheme(themeName, {
        primary: primaryColor,
        secondary: '#FF6B6B',
        background: '#FFFFFF',
        surface: '#F5F5F5',
        text: '#222222',
        textSecondary: '#666666',
      });
      setThemeName('');
    }
  };
 
  return (
    <div style={{ padding: '20px' }}>
      <input
        type="text"
        value={themeName}
        onChange={(e) => setThemeName(e.target.value)}
        placeholder="Theme name"
      />
      <input
        type="color"
        value={primaryColor}
        onChange={(e) => setPrimaryColor(e.target.value)}
      />
      <button onClick={handleCreate}>Create Theme</button>
    </div>
  );
}

Integration with Other Features

Applying consistent theming across camera and gallery screens is covered in Rork Max Camera & Gallery Guide.

For comprehensive design system alignment with design tools, see Rork Max Figma Integration Guide. Learn how to animate theme transitions smoothly in Rork Max Animations Guide.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-10-30
It runs in development but the iOS Release build dies on launch — checking whether a missing scheme is the cause
In an expo-router app, an app.json with no scheme can crash only the iOS Release build at launch. Why Debug and Android survive, the two ways to fix it, how to think about leaving the scheme out on purpose, and a pre-submission check.
⬡ Dev Tools2026-10-29
When DevTools Stays Silent in Expo Go on Android, Suspect the Runtime Before Your Code
In Expo Go on Android, React Native DevTools can open but never evaluate anything in the Console, and the Sources tab stays empty. Three quick checks to tell it apart from a bug in your own code, how to switch to a development build, and what to do if you can't.
⬡ Dev Tools2026-10-28
Users Told Me They Could Not Close a Fullscreen Ad. The Entry Points Were Scattered, So I Put the Show-or-Not Check and a Safety Net in One Place
An unclosable fullscreen ad came from calls that ignored foreground state, a clash with a permission dialog, and no safety net. A single entry-point component, tested in Node.