UTA DevHub

Font Management Guide

Comprehensive guide for managing custom fonts in React Native applications including loading strategies, fallbacks, and performance optimization

Font Management Guide

Overview

This guide provides best practices for loading and configuring custom fonts in React Native applications, including loading strategies, fallback mechanisms, performance optimization, and platform-specific considerations.

Scope: This guide focuses on font asset loading and technical setup. For typography scales, font sizes, and design system integration, see Theme Management.

Font Loading Strategies

Expo Font Loading

Basic Font Loading with Expo

// App.tsx
import * as Font from 'expo-font';
import { useState, useEffect } from 'react';
 
const loadFonts = async () => {
  await Font.loadAsync({
    'Roboto-Regular': require('@/assets/fonts/Roboto-Regular.ttf'),
    'Roboto-Bold': require('@/assets/fonts/Roboto-Bold.ttf'),
    'Roboto-Light': require('@/assets/fonts/Roboto-Light.ttf'),
    'CustomIcons': require('@/assets/fonts/CustomIcons.ttf'),
  });
};
 
export default function App() {
  const [fontsLoaded, setFontsLoaded] = useState(false);
 
  useEffect(() => {
    loadFonts().then(() => setFontsLoaded(true));
  }, []);
 
  if (!fontsLoaded) {
    return <SplashScreen />;
  }
 
  return <MainApp />;
}

React Native CLI Font Setup

Place Font Files

Copy font files to the appropriate platform directories

# iOS
ios/YourApp/fonts/
├── Inter-Regular.ttf
├── Inter-Bold.ttf
└── Inter-SemiBold.ttf

# Android
android/app/src/main/assets/fonts/
├── Inter-Regular.ttf
├── Inter-Bold.ttf
└── Inter-SemiBold.ttf

Configure iOS Info.plist

Add font declarations to iOS configuration

<!-- ios/YourApp/Info.plist -->
<key>UIAppFonts</key>
<array>
  <string>Inter-Regular.ttf</string>
  <string>Inter-Bold.ttf</string>
  <string>Inter-SemiBold.ttf</string>
</array>

Run linking command for automatic setup

# React Native 0.60+ (auto-linking)
npx react-native run-ios
npx react-native run-android
 
# Older versions
npx react-native link

Test Font Loading

Verify fonts are available in your app

// Test component
const FontTest = () => (
  <View>
    <Text style={{ fontFamily: 'Inter-Regular' }}>Regular Text</Text>
    <Text style={{ fontFamily: 'Inter-Bold' }}>Bold Text</Text>
    <Text style={{ fontFamily: 'Inter-SemiBold' }}>SemiBold Text</Text>
  </View>
);

Font Organization and Constants

Font Constants Pattern

Typography Scale Integration: For font sizes, weights, and design system typography, use the centralized theme management system. See Theme Implementation for the canonical typography definitions.

// core/constants/fonts.ts
export const Fonts = {
  // Primary font family
  primary: {
    regular: 'Inter-Regular',
    medium: 'Inter-Medium',
    semiBold: 'Inter-SemiBold',
    bold: 'Inter-Bold',
  },
  
  // Secondary font family (if needed)
  secondary: {
    regular: 'Roboto-Regular',
    medium: 'Roboto-Medium',
    bold: 'Roboto-Bold',
  },
  
  // Icon fonts
  icons: {
    custom: 'CustomIcons',
    material: 'MaterialIcons',
  },
  
  // System font fallbacks
  system: {
    ios: 'San Francisco',
    android: 'Roboto',
    default: 'System',
  }
};

Design System Integration: Instead of defining font sizes and weights here, import them from the theme management system to maintain consistency across your application.

// Use theme management for typography scale
import { useTheme } from '@core/shared/styles';
 
// Example: Integrating with theme management
const ProductCard = () => {
  const theme = useTheme();
  
  return (
    <Text style={theme.textStyles.body}>
      Typography using design system
    </Text>
  );
};

Typography Component Integration

Recommended Approach: Use the centralized Typography component from the theme management system instead of creating custom typography components. This ensures consistency with your design system.

Naming Standards: Follow our File Naming Conventions and Project Structure when creating components. Use concise, descriptive names that align with React community practices.

// Recommended: Use theme management Typography
import { useTheme } from '@core/shared/styles';
 
const ProductDetail = () => {
  const theme = useTheme();
  
  return (
    <View>
      <Text style={theme.textStyles.h1}>Product Name</Text>
      <Text style={theme.textStyles.body}>Product description</Text>
      <Text style={theme.textStyles.caption}>Price details</Text>
    </View>
  );
};

If you need custom typography components that integrate with font loading, combine font management with theme system:

// ui/foundation/Text/Text.tsx
import React from 'react';
import { Text as RNText, TextProps, Platform } from 'react-native';
import { useTheme } from '@core/shared/styles';
import { useFonts } from '@/hooks/useFonts';
import { Fonts } from '@/core/constants/fonts';
 
interface CustomTextProps extends TextProps {
  variant?: 'h1' | 'h2' | 'h3' | 'body' | 'bodySmall' | 'button' | 'caption';
}
 
export const Text: React.FC<CustomTextProps> = ({
  variant = 'body',
  style,
  children,
  ...props
}) => {
  const theme = useTheme();
  const { fontsLoaded } = useFonts();
  
  // Get theme text styles
  const baseStyle = theme.textStyles[variant] || theme.textStyles.body;
  
  // Apply fallback font if custom fonts aren't loaded
  const textStyle = fontsLoaded 
    ? baseStyle
    : {
        ...baseStyle,
        fontFamily: Platform.select({
          ios: Fonts.system.ios,
          android: Fonts.system.android,
          default: Fonts.system.default,
        }),
      };
 
  return (
    <RNText style={[textStyle, style]} {...props}>
      {children}
    </RNText>
  );
};

Platform-Specific Considerations

iOS Font Configuration

Configuring iOS Info.plist

<!-- ios/YourApp/Info.plist -->
<dict>
  <!-- Other keys -->
  <key>UIAppFonts</key>
  <array>
    <string>Inter-Regular.ttf</string>
    <string>Inter-Medium.ttf</string>
    <string>Inter-SemiBold.ttf</string>
    <string>Inter-Bold.ttf</string>
    <string>CustomIcons.ttf</string>
  </array>
</dict>

Font File Names: Use the exact filename (including extension) as it appears in your bundle, not the PostScript name.

Android Font Configuration

// Platform-specific font handling for Android
const getAndroidFontFamily = (family: string, weight: string) => {
  // Android can use fontWeight property with single family
  return {
    fontFamily: family,
    fontWeight: weight,
  };
};
 
// Platform-agnostic font style helper
export const getFontStyle = (family: string, weight: string) => {
  return Platform.select({
    ios: {
      fontFamily: getIOSFontFamily(family, weight),
    },
    android: getAndroidFontFamily(family, weight),
    default: {
      fontFamily: family,
      fontWeight: weight,
    },
  });
};

Font Fallback Strategies

Graceful Degradation

// core/utils/fontFallback.ts
import { Platform } from 'react-native';
import { Fonts } from '@/core/constants/fonts';
 
export const FontFallback = {
  getFont(
    preferredFont: string,
    fallbackFont?: string
  ): string {
    // Check if font is loaded (Expo only)
    if (typeof Font !== 'undefined' && Font.isLoaded?.(preferredFont)) {
      return preferredFont;
    }
    
    // Use provided fallback
    if (fallbackFont) {
      return fallbackFont;
    }
    
    // Use system font as last resort
    return Platform.select({
      ios: Fonts.system.ios,
      android: Fonts.system.android,
      default: Fonts.system.default,
    });
  },
  
  createFontStack(fonts: string[]): string {
    // Create font family stack for CSS-like fallback
    return fonts.join(', ');
  }
};
 
// Usage in styles
const styles = StyleSheet.create({
  title: {
    fontFamily: FontFallback.getFont('Inter-Bold', 'System'),
    fontSize: 24,
  },
  
  body: {
    fontFamily: FontFallback.createFontStack([
      'Inter-Regular',
      'San Francisco',
      'Roboto',
      'System'
    ]),
    fontSize: 16,
  },
});

Loading State Management

// components/FontAwareText.tsx
import React from 'react';
import { Text, TextProps } from 'react-native';
import { useFonts } from '@/hooks/useFonts';
import { FontFallback } from '@/core/utils/fontFallback';
 
interface FontAwareTextProps extends TextProps {
  fontFamily?: string;
  fallbackFont?: string;
}
 
export const FontAwareText: React.FC<FontAwareTextProps> = ({
  fontFamily = 'Inter-Regular',
  fallbackFont,
  style,
  children,
  ...props
}) => {
  const { fontsLoaded } = useFonts();
  
  const resolvedFontFamily = fontsLoaded
    ? fontFamily
    : FontFallback.getFont(fontFamily, fallbackFont);
  
  return (
    <Text
      style={[{ fontFamily: resolvedFontFamily }, style]}
      {...props}
    >
      {children}
    </Text>
  );
};

Performance Optimization

Font Loading Performance

Performance Impact: Loading many custom fonts can significantly impact app startup time. Follow these strategies to minimize the impact.

Essential vs Optional Fonts

// Prioritize critical fonts for immediate loading
const FontLoader = {
  async loadEssentialFonts() {
    // Load only fonts needed for initial screen
    await Font.loadAsync({
      'Inter-Regular': require('@/assets/fonts/Inter-Regular.ttf'),
      'Inter-Bold': require('@/assets/fonts/Inter-Bold.ttf'),
    });
  },
  
  async loadOptionalFonts() {
    // Load additional fonts in background
    await Font.loadAsync({
      'Inter-Light': require('@/assets/fonts/Inter-Light.ttf'),
      'Inter-SemiBold': require('@/assets/fonts/Inter-SemiBold.ttf'),
      'CustomIcons': require('@/assets/fonts/CustomIcons.ttf'),
    });
  }
};
 
// In your app
export default function App() {
  const [essentialFontsLoaded, setEssentialFontsLoaded] = useState(false);
  
  useEffect(() => {
    FontLoader.loadEssentialFonts()
      .then(() => setEssentialFontsLoaded(true));
    
    // Load optional fonts in background
    FontLoader.loadOptionalFonts();
  }, []);
  
  if (!essentialFontsLoaded) {
    return <SplashScreen />;
  }
  
  return <MainApp />;
}

Bundle Size Optimization

Font Subsetting

// Only include characters you need
const FontOptimization = {
  // Analyze text usage to determine required character set
  analyzeCharacterUsage(texts: string[]): Set<string> {
    const chars = new Set<string>();
    texts.forEach(text => {
      for (const char of text) {
        chars.add(char);
      }
    });
    return chars;
  },
  
  // Generate subset specification for font optimization tools
  generateSubsetSpec(chars: Set<string>): string {
    return Array.from(chars).join('');
  }
};

Font Format Optimization

# Use tools like fonttools to create optimized font files
pip install fonttools
 
# Subset font to specific characters
pyftsubset Inter-Regular.ttf \
  --text="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789" \
  --output-file=Inter-Regular-subset.ttf
 
# Convert to WOFF2 for web (if using web components)
fonttools ttLib.woff2 compress Inter-Regular.ttf

Custom Icon Fonts

Creating Icon Fonts

Design Icons

Create SVG icons following these guidelines:

  • Use consistent artboard size (24x24px recommended)
  • Maintain uniform stroke width (2px)
  • Use simple paths without complex effects
  • Name files descriptively (arrow-left.svg, user-profile.svg)

Generate Icon Font

Use tools like IcoMoon or Fontello to create icon fonts

# Using fontello-cli
npm install -g fontello-cli
 
# Upload SVGs and download font
fontello-cli install --config fontello-config.json

Integrate Icon Font

Add the generated font to your React Native app

// core/constants/icons.ts
export const IconFont = {
  family: 'CustomIcons',
  glyphs: {
    home: '\ue900',
    profile: '\ue901',
    settings: '\ue902',
    search: '\ue903',
  }
};
 
// components/IconText.tsx
export const IconText = ({ icon, size = 24, color = '#000' }) => (
  <Text style={{
    fontFamily: IconFont.family,
    fontSize: size,
    color,
  }}>
    {IconFont.glyphs[icon]}
  </Text>
);

Testing Font Implementation

Font Loading Tests

// __tests__/fonts.test.ts
import { FontService } from '@/core/services/fontService';
import { Fonts } from '@/core/constants/fonts';
 
describe('Font Management', () => {
  it('should load all essential fonts', async () => {
    await FontService.loadFonts();
    
    // Test that fonts are available
    expect(FontService.isLoaded('Inter-Regular')).toBe(true);
    expect(FontService.isLoaded('Inter-Bold')).toBe(true);
  });
  
  it('should handle font loading failures gracefully', async () => {
    // Mock font loading failure
    jest.spyOn(Font, 'loadAsync').mockRejectedValue(new Error('Font load failed'));
    
    // Should not throw error
    await expect(FontService.loadFonts()).resolves.not.toThrow();
  });
  
  it('should provide correct font family constants', () => {
    expect(Fonts.primary.regular).toBeDefined();
    expect(Fonts.primary.bold).toBeDefined();
    expect(typeof Fonts.primary.regular).toBe('string');
  });
});

Typography Component Tests

// __tests__/Typography.test.tsx
import React from 'react';
import { render } from '@testing-library/react-native';
import { Typography } from '@/ui/foundation/Typography/Typography';
 
describe('Typography Component', () => {
  it('should render with correct font family', () => {
    const { getByTestId } = render(
      <Typography testID="typography" variant="heading1">
        Test Text
      </Typography>
    );
    
    const element = getByTestId('typography');
    expect(element.props.style).toMatchObject({
      fontFamily: expect.stringContaining('Inter'),
    });
  });
  
  it('should handle font fallbacks', () => {
    const { getByTestId } = render(
      <Typography testID="typography" family="system">
        Test Text
      </Typography>
    );
    
    const element = getByTestId('typography');
    // Should fall back to system font
    expect(element.props.style.fontFamily).toBeDefined();
  });
});

Common Font Issues

Issue 1: Fonts Not Loading on iOS

Problem: Custom fonts appear as system fonts on iOS Solutions:

// ❌ Common mistakes
// Using PostScript name instead of filename
'InterRegular' // Wrong
 
// ✅ Correct approach
// Use exact filename in Info.plist and code
'Inter-Regular' // Correct filename

Issue 2: Font Weight Not Working

Problem: Different font weights appear identical Solutions:

// ❌ BAD: Relying on fontWeight property only
{
  fontFamily: 'Inter',
  fontWeight: 'bold', // May not work on iOS
}
 
// ✅ GOOD: Use specific font family names
{
  fontFamily: Platform.select({
    ios: 'Inter-Bold',
    android: 'Inter',
  }),
  fontWeight: Platform.select({
    ios: undefined,
    android: 'bold',
  }),
}

Issue 3: Missing Character Glyphs

Problem: Some characters display as squares or missing glyphs Solutions:

// Check character support in font
const hasCharacterSupport = (font: string, character: string): boolean => {
  // Implementation to check if font supports character
  // Use fallback font for unsupported characters
  return true; // Simplified
};
 
// Provide fallback for unsupported characters
const TextWithFallback = ({ text, primaryFont, fallbackFont }) => {
  const hasSupport = hasCharacterSupport(primaryFont, text);
  return (
    <Text style={{ fontFamily: hasSupport ? primaryFont : fallbackFont }}>
      {text}
    </Text>
  );
};