UTA DevHub
UI Development/Theme Management

NativeWind Theme Integration

Implementation guide for integrating NativeWind (TailwindCSS for React Native) with our theme management system.

NativeWind Theme Integration

Overview

This guide explains how to integrate NativeWind (TailwindCSS for React Native) with our existing theme management system. By properly configuring both systems to work together, developers can leverage the utility-first approach of TailwindCSS while maintaining the benefits of our dynamic theming architecture.

NativeWind brings the utility-first CSS paradigm of TailwindCSS to React Native, enabling rapid UI development with pre-defined utility classes. Our integration approach ensures that NativeWind's styling remains consistent with our theme system's values and responds correctly to theme changes.

Architecture

The integration between our theme system and NativeWind is built around three key components:

  1. TailwindCSS Configuration: Using our theme tokens as the source of truth for TailwindCSS configuration values
  2. Theme Bridge Component: A wrapper that syncs theme changes between our system and NativeWind
  3. Styled Component Patterns: Consistent approaches for combining both styling methods

Getting Started

Prerequisites

Before integrating NativeWind with our theme system, ensure you have:

Installation and Setup

  1. Install NativeWind and its peer dependencies:
npm install nativewind tailwindcss@^3.4.17 react-native-reanimated@3.16.2 react-native-safe-area-context
  1. Initialize TailwindCSS configuration:
npx tailwindcss init
  1. Configure Babel preset by updating your babel.config.js:
// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: [
      ["babel-preset-expo", { jsxImportSource: "nativewind" }],
      "nativewind/babel",
    ],
  };
};

Tailwind Configuration with Theme Tokens

The first step in the integration is to configure TailwindCSS to use our theme tokens. This ensures a single source of truth for all styling values in the application.

Basic Configuration

Update your tailwind.config.js file to import and use our theme tokens:

// tailwind.config.js
const baseTheme = require('./src/core/shared/styles/theme');
 
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./App.tsx", "./src/**/*.{js,jsx,ts,tsx}"],
  presets: [require("nativewind/preset")],
  theme: {
    // Use our theme colors
    colors: {
      primary: baseTheme.colors.primary,
      secondary: baseTheme.colors.secondary,
      background: baseTheme.colors.background,
      text: baseTheme.colors.text,
      // Map other colors from our theme
    },
    // Use our theme spacing
    spacing: {
      xs: baseTheme.spacing.xs + 'px',
      s: baseTheme.spacing.s + 'px',
      m: baseTheme.spacing.m + 'px',
      l: baseTheme.spacing.l + 'px',
      xl: baseTheme.spacing.xl + 'px',
      // Map other spacing values
    },
    // Map other theme properties as needed
    extend: {},
  },
  plugins: [],
};

Create CSS File

Create a CSS file (e.g., global.css) with the Tailwind directives:

/* global.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

Configure Metro

Modify your metro.config.js to use NativeWind:

// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withNativeWind } = require('nativewind/metro');
 
const config = getDefaultConfig(__dirname)
 
module.exports = withNativeWind(config, { input: './global.css' })

Import CSS File

Import the CSS file in your App component:

// App.tsx
import "./global.css";
 
export default function App() {
  return (
    // Your app content
  );
}

Dark Mode Configuration

Configure dark mode support to work with our theme system:

// tailwind.config.js
module.exports = {
  // ... other config
  presets: [require("nativewind/preset")],
  theme: {
    // ... base theme
    extend: {
      colors: {
        // Define dark mode specific colors if needed
      }
    }
  }
};

Creating a Theme Bridge

To sync theme changes between our theme system and NativeWind, we need to create a bridge component:

// ThemeNativeWindBridge.tsx
import React, { useEffect } from 'react';
import { useTheme } from '@core/shared/styles';
import { useColorScheme } from 'nativewind';
 
interface ThemeNativeWindBridgeProps {
  children: React.ReactNode;
}
 
export function ThemeNativeWindBridge({ children }: ThemeNativeWindBridgeProps) {
  const theme = useTheme();
  const { colorScheme, setColorScheme } = useColorScheme();
  
  // When our theme changes, update NativeWind
  useEffect(() => {
    // Map our theme mode to NativeWind color scheme
    const newColorScheme = theme.mode === 'dark' ? 'dark' : 'light';
    if (colorScheme !== newColorScheme) {
      setColorScheme(newColorScheme);
    }
  }, [theme.mode, colorScheme, setColorScheme]);
  
  return <>{children}</>;
}

Integration with App Structure

Place the bridge component in your application hierarchy with the NativeWindProvider:

// App.tsx
import "./global.css";
import { ThemeProvider } from '@core/shared/styles';
import { NativeWindProvider } from 'nativewind';
import { ThemeNativeWindBridge } from './ThemeNativeWindBridge';
 
export default function App() {
  return (
    <ThemeProvider>
      <NativeWindProvider>
        <ThemeNativeWindBridge>
          {/* Your app content */}
        </ThemeNativeWindBridge>
      </NativeWindProvider>
    </ThemeProvider>
  );
}

Component Styling Patterns

There are several approaches to combining our theme system with NativeWind in components:

1. NativeWind-First Approach

Use NativeWind for most styling, with our theme system for dynamic values:

import { useTheme } from '@core/shared/styles';
import { View, Text } from 'react-native';
 
export function CardComponent() {
  const theme = useTheme();
  
  return (
    <View className="p-4 rounded-lg bg-white dark:bg-gray-800">
      <Text 
        className="text-lg font-bold" 
        style={{ color: theme.colors.primary }} // Dynamic value from theme
      >
        Card Title
      </Text>
      <Text className="text-gray-600 dark:text-gray-300">
        Card content goes here
      </Text>
    </View>
  );
}

2. Theme-First Approach

Use our theme system for component structure and NativeWind for utility classes:

import { useTheme } from '@core/shared/styles';
import { StyleSheet } from 'react-native';
import { View, Text } from 'react-native';
 
export function CardComponent() {
  const theme = useTheme();
  const styles = StyleSheet.create({
    container: {
      backgroundColor: theme.colors.cardBackground,
      padding: theme.spacing.m,
      borderRadius: theme.radius.m,
    },
    title: {
      color: theme.colors.text,
      fontSize: theme.typography.sizes.lg,
      fontWeight: 'bold',
    },
    content: {
      color: theme.colors.textSecondary,
    },
  });
  
  return (
    <View style={styles.container} className="shadow-md">
      <Text style={styles.title}>Card Title</Text>
      <Text style={styles.content} className="mt-2">
        Card content goes here
      </Text>
    </View>
  );
}

3. Hybrid Utility Function

Create utility functions that generate styles using both systems:

// useHybridStyles.ts
import { useTheme } from '@core/shared/styles';
import { StyleSheet } from 'react-native';
import { useColorScheme } from 'nativewind';
 
export function useHybridStyles() {
  const theme = useTheme();
  const { colorScheme } = useColorScheme();
  const isDark = colorScheme === 'dark';
  
  return {
    createStyles: (stylesFn) => {
      return StyleSheet.create(stylesFn(theme, isDark));
    },
    theme,
    isDark,
  };
}
 
// Usage in component
function MyComponent() {
  const { createStyles, isDark } = useHybridStyles();
  
  const styles = createStyles((theme, isDark) => ({
    container: {
      backgroundColor: isDark ? theme.colors.darkBackground : theme.colors.background,
      padding: theme.spacing.m,
    },
  }));
  
  return (
    <View style={styles.container} className={isDark ? "shadow-white" : "shadow-black"}>
      {/* Component content */}
    </View>
  );
}

White-Label Integration

NativeWind's CSS variable system works perfectly with our white-label theming approach. Here's how to implement a complete solution:

Configure CSS Variables in Tailwind

First, set up your Tailwind configuration to use CSS variables for theme values:

// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: ["./App.tsx", "./src/**/*.{js,jsx,ts,tsx}"],
  presets: [require("nativewind/preset")],
  theme: {
    colors: {
      // Define brand colors using CSS variables
      primary: "rgb(var(--color-primary) / <alpha-value>)",
      secondary: "rgb(var(--color-secondary) / <alpha-value>)",
      background: "rgb(var(--color-background) / <alpha-value>)",
      text: "rgb(var(--color-text) / <alpha-value>)",
      // Other theme colors
    },
    // Other theme properties
  },
  plugins: [
    // Set default values
    ({ addBase }) => 
      addBase({
        ":root": {
          // Default brand colors (RGB format)
          "--color-primary": "25 118 210",     // #1976d2
          "--color-secondary": "156 39 176",   // #9c27b0
          "--color-background": "255 255 255", // #ffffff
          "--color-text": "33 33 33",          // #212121
          // Other default values
        },
      }),
  ],
};

Create Brand Themes with CSS Variables

Define your brand themes using NativeWind's vars function:

// BrandThemes.ts
import { vars } from 'nativewind';
import { BrandConfig } from '@core/domains/white-label/types';
 
// Helper to convert hex color to RGB values
function hexToRgb(hex: string): string {
  // Remove # if present
  hex = hex.replace('#', '');
  
  // Parse hex values
  const r = parseInt(hex.substring(0, 2), 16);
  const g = parseInt(hex.substring(2, 4), 16);
  const b = parseInt(hex.substring(4, 6), 16);
  
  // Return RGB string
  return `${r} ${g} ${b}`;
}
 
// Create theme variables for each brand
export function createBrandTheme(brandConfig: BrandConfig, isDark: boolean = false) {
  const config = isDark ? brandConfig.darkMode : brandConfig;
  
  return vars({
    '--color-primary': hexToRgb(config.colors.primary),
    '--color-secondary': hexToRgb(config.colors.secondary),
    '--color-background': hexToRgb(config.colors.background),
    '--color-text': hexToRgb(config.colors.text),
    // Other brand-specific values
  });
}

Enhanced Bridge Component

Create a more robust bridge component that applies both theme mode and brand variables:

// ThemeNativeWindBridge.tsx
import React, { useMemo } from 'react';
import { View } from 'react-native';
import { useTheme } from '@core/shared/styles';
import { useWhiteLabelConfig } from '@core/domains/white-label';
import { useColorScheme } from 'nativewind';
import { createBrandTheme } from './BrandThemes';
 
interface ThemeNativeWindBridgeProps {
  children: React.ReactNode;
}
 
export function ThemeNativeWindBridge({ children }: ThemeNativeWindBridgeProps) {
  const theme = useTheme();
  const { brandConfig } = useWhiteLabelConfig();
  const { colorScheme, setColorScheme } = useColorScheme();
  
  // Set color scheme based on theme mode
  React.useEffect(() => {
    const newColorScheme = theme.mode === 'dark' ? 'dark' : 'light';
    if (colorScheme !== newColorScheme) {
      setColorScheme(newColorScheme);
    }
  }, [theme.mode, colorScheme, setColorScheme]);
  
  // Create brand variables based on current brand and theme mode
  const brandVariables = useMemo(() => {
    const isDark = theme.mode === 'dark';
    return createBrandTheme(brandConfig, isDark);
  }, [brandConfig, theme.mode]);
  
  // Apply both color scheme and brand variables
  return (
    <View style={brandVariables} className="flex-1">
      {children}
    </View>
  );
}

Using Brand Themes in Components

Now you can use your brand-aware components with NativeWind:

// BrandAwareButton.tsx
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
 
interface ButtonProps {
  label: string;
  onPress: () => void;
  variant?: 'primary' | 'secondary';
}
 
export function BrandAwareButton({ label, onPress, variant = 'primary' }: ButtonProps) {
  const baseClasses = "py-3 px-4 rounded-md items-center justify-center";
  const variantClasses = variant === 'primary' 
    ? "bg-primary" 
    : "bg-secondary";
    
  return (
    <TouchableOpacity 
      className={`${baseClasses} ${variantClasses}`}
      onPress={onPress}
    >
      <Text className="text-white font-bold text-base">
        {label}
      </Text>
    </TouchableOpacity>
  );
}

Performance Considerations

Using both styling systems together can impact performance. Here are some best practices:

Do ✅

  • Memoize styles generated from both systems
  • Use React.memo for components with complex styling
  • Prefer static NativeWind classes for unchanging styles
  • Use our theme system only for truly dynamic values
// Good example
const MemoizedCard = React.memo(function Card({ title, content }) {
  const theme = useTheme();
  // Only recalculate styles when theme changes
  const dynamicStyles = React.useMemo(() => ({
    titleColor: { color: theme.colors.primary }
  }), [theme.colors.primary]);
  
  return (
    <View className="p-4 rounded-lg bg-white dark:bg-gray-800">
      <Text className="text-lg font-bold" style={dynamicStyles.titleColor}>
        {title}
      </Text>
      <Text className="text-gray-600 dark:text-gray-300">
        {content}
      </Text>
    </View>
  );
});

Don't ❌

  • Mix both systems for the same style property
  • Generate NativeWind classes dynamically
  • Create complex conditional classes inside render functions
// Bad example - avoid this
function Card({ title, content }) {
  const theme = useTheme();
  // This is inefficient - mixing systems and recalculating each render
  return (
    <View 
      className={`p-${theme.isCompact ? '2' : '4'} rounded-lg`} 
      style={{ backgroundColor: theme.colors.cardBackground }}
    >
      <Text style={{ color: theme.colors.text, fontSize: 18 }}>
        {title}
      </Text>
      <Text className={theme.mode === 'dark' ? 'text-gray-300' : 'text-gray-600'}>
        {content}
      </Text>
    </View>
  );
}

Migration Strategy

When migrating existing components to use NativeWind, follow this approach:

  1. Start with non-dynamic components that don't rely heavily on theme changes
  2. Incrementally adopt NativeWind for appropriate style properties
  3. Create a style guide for which properties should use which system

Migration Example

// Before: Theme-only styling
function Button({ onPress, label }) {
  const theme = useTheme();
  const styles = StyleSheet.create({
    button: {
      backgroundColor: theme.colors.primary,
      padding: theme.spacing.m,
      borderRadius: theme.radius.m,
      alignItems: 'center',
      justifyContent: 'center',
    },
    label: {
      color: theme.colors.buttonText,
      fontSize: theme.typography.sizes.m,
      fontWeight: 'bold',
    },
  });
  
  return (
    <TouchableOpacity style={styles.button} onPress={onPress}>
      <Text style={styles.label}>{label}</Text>
    </TouchableOpacity>
  );
}

Troubleshooting

Common issues when integrating NativeWind with our theme system:

IssuePossible CauseSolution
Styles not applyingBabel preset not configuredCheck babel.config.js and metro.config.js
Dark mode not workingTheme bridge not updatingVerify useEffect dependencies in bridge component
Inconsistent stylingMixed styling approachesFollow the style guide for consistent property assignment
Performance issuesExcessive style recalculationsUse memoization and static classes where possible

Core References

External Resources

Summary

Integrating NativeWind with our theme management system provides the best of both worlds - the utility-first approach of TailwindCSS with the dynamic theming capabilities of our custom system. By following the patterns outlined in this guide, developers can create components that are both rapidly styled and responsive to theme changes.

The key to successful integration is maintaining a single source of truth for theme values while leveraging each system's strengths. With proper configuration and the bridge component in place, both systems can work together seamlessly to create a consistent, brand-aware UI experience.