Custom fonts provide your React Native applications with a unique visual identity that sets them apart from apps using default system fonts. Whether you're building a brand-forward consumer app or a professional business application, typography plays a crucial role in user experience and brand recognition.
This comprehensive guide walks you through the modern approaches for integrating custom fonts into both Expo and React Native CLI projects, covering everything from basic file placement to advanced optimization techniques.
Modern React Native development offers multiple pathways for font integration, each suited to different project requirements and development workflows. Understanding these options empowers developers to make informed decisions based on their specific use cases, performance requirements, and platform targets. For teams focused on delivering exceptional mobile experiences, partnering with professional web development services ensures consistent typography implementation across all application touchpoints.
Supported Font Formats
Before diving into implementation, understanding supported font formats ensures compatibility across all target platforms. Expo SDK officially supports OTF (OpenType Font) and TTF (TrueType Font) formats across Android, iOS, and web platforms. These formats provide the broadest cross-platform compatibility and are recommended for most production applications.
OTF files are generally preferred over TTF when both formats are available for a given font. OTF files typically have smaller file sizes compared to their TTF counterparts, which contributes to reduced app bundle sizes. Additionally, OTF fonts sometimes render slightly better in certain contexts due to advanced typographic features supported by the format.
For iOS-specific projects, additional web font formats receive support including WOFF (Web Open Font Format) and WOFF2. These formats offer superior compression and are particularly useful for web-based React Native deployments where bandwidth considerations matter. However, Android does not support WOFF formats through the standard expo-font config plugin, limiting their utility in cross-platform applications.
Variable fonts, including variable font implementations in OTF and TTF formats, do not have full support across all platforms. For applications requiring consistent typography across all platforms, using static fonts remains the recommended approach. Developers needing variable font features can use utilities such as fontTools to extract specific axis configurations and save them as separate static font files.
Method One: Using Local Font Files with Config Plugin
The expo-font config plugin represents the recommended approach for embedding fonts in production applications. This method integrates fonts directly into your application's native code, providing immediate availability when the app launches without requiring runtime loading steps.
Benefits of Config Plugin Approach
Embedding fonts via config plugin offers several significant advantages for production applications. Fonts become available immediately when the app starts on a device, eliminating the flash of unstyled text that can occur when fonts load asynchronously at runtime. This immediate availability creates a more polished user experience, particularly during application launch sequences.
Since fonts are bundled within the application, they remain consistently available across all devices where the app is installed. Unlike runtime-loaded fonts that might fail to load under poor network conditions, embedded fonts always render correctly regardless of connectivity status. This reliability proves essential for applications requiring consistent branding across diverse user environments.
The config plugin approach also eliminates the need for additional loading state management in your components. Without runtime loading, you avoid implementing font loading indicators, handling loading errors, or managing the transition period between font loading and font availability. This simplification reduces code complexity and potential points of failure.
Implementation Steps
Begin by installing the expo-font library in your project using Expo's CLI:
npx expo install expo-font
Next, add the expo-font plugin to your app configuration file. The configuration specifies the path to your font files relative to the project root. Conventionally, font files reside in an assets/fonts directory, though alternative locations work with appropriate path adjustments:
{
"expo": {
"plugins": [
[
"expo-font",
{
"fonts": ["./assets/fonts/CustomFont.ttf"]
}
]
]
}
}
For multiple fonts or explicit font family names, use the extended configuration:
{
"expo": {
"plugins": [
[
"expo-font",
{
"fonts": [
{
"fontFamily": "BrandFont",
"android": { "weight": 400, "style": "normal" },
"ios": { "fontFamily": "BrandFont-Regular" },
"web": { "fontFamily": "BrandFont" },
"paths": ["./assets/fonts/BrandFont-Regular.ttf"]
},
{
"fontFamily": "BrandFontBold",
"android": { "weight": 700, "style": "normal" },
"ios": { "fontFamily": "BrandFont-Bold" },
"web": { "fontFamily": "BrandFont-Bold" },
"paths": ["./assets/fonts/BrandFont-Bold.ttf"]
}
]
}
]
]
}
}
After configuring your app config, rebuild your development build or production build to incorporate the embedded fonts. This rebuilding step is essential because the config plugin modifies native project files during the build process. Development builds created through expo-dev-client support embedded fonts, while standard Expo Go applications do not.
Platform-Specific Behavior
The config plugin handles platform differences automatically while providing appropriate defaults. On Android, when you specify fontFamily names explicitly, the plugin creates native XML font resources that integrate with the Android font system. When providing only file paths, Android uses the filename as the font family name, which may require filename conventions matching your desired fontFamily values.
iOS always extracts the font family name directly from the font file metadata, making it less dependent on configuration-provided names. This behavior means the fontFamily name you reference in your JavaScript code must match the actual font family name embedded in the font file itself.
Method Two: Runtime Loading with useFonts Hook
The useFonts hook provides runtime font loading that works across Android, iOS, and web platforms. This approach loads font files when needed rather than embedding them in the application bundle, offering flexibility for applications with many font variations or those practicing progressive font loading.
Basic Usage Pattern
The useFonts hook accepts a font map object where keys represent font family names and values represent font file paths or URL references. The hook returns a loading boolean and error value, enabling conditional rendering based on font availability:
import { useFonts } from 'expo-font';
export default function App() {
const [fontsLoaded, error] = useFonts({
'CustomFont': require('./assets/fonts/CustomFont.ttf'),
'AnotherFont': 'https://fonts.gstatic.com/s/anotherfont/v1/AnotherFont-Regular.ttf',
'BrandFont': { uri: 'https://example.com/fonts/BrandFont-Regular.ttf' },
});
if (error) {
console.error('Font loading failed:', error);
return <Text>Failed to load fonts</Text>;
}
if (!fontsLoaded) {
return <LoadingSpinner />;
}
return <YourAppContent />;
}
This pattern enables sophisticated font loading strategies including preloading fonts before navigation, lazy loading fonts for secondary screens, and gracefully handling font loading failures without breaking the user experience.
Performance Considerations
Runtime font loading impacts perceived performance differently than config plugin embedding. Fonts load asynchronously, potentially causing momentary text rendering with fallback system fonts before custom fonts appear. For critical brand typography, consider implementing loading overlays or styled text components that maintain visual consistency during the loading phase.
Bundle size benefits represent a key advantage of runtime loading. Rather than embedding all fonts in the initial application download, fonts load on demand. This approach particularly benefits applications with extensive typography needs or those targeting users in regions with slower connections.
Network reliability affects runtime loading reliability. Applications loading fonts from remote URLs must handle network failures gracefully, implementing retry logic and appropriate fallback behavior. For maximum reliability, consider caching loaded fonts using React Native's AsyncStorage or a custom caching solution.
Google Fonts Integration
Expo provides streamlined integration for Google Fonts through dedicated packages. The @expo-google-fonts family of packages offers pre-configured font loading for hundreds of Google Fonts, dramatically simplifying the integration process for these widely-used typefaces.
Installing Google Font Packages
Google Font packages follow a consistent naming convention using the format @expo-google-fonts/[font-name]. For example, the Inter font package is available as @expo-google-fonts/inter. Installation follows standard npm conventions:
npx expo install @expo-google-fonts/inter @expo-google-fonts/roboto @expo-google-fonts/playfair-display
Each font package exports individual font hooks and load functions for fine-grained control over loading behavior:
import { useFonts } from 'expo-font';
import { Inter_400Regular, Inter_700Bold, PlayfairDisplay_600SemiBold } from '@expo-google-fonts/inter';
export default function App() {
const [fontsLoaded] = useFonts({
'Inter-Regular': Inter_400Regular,
'Inter-Bold': Inter_700Bold,
'Playfair-SemiBold': PlayfairDisplay_600SemiBold,
});
if (!fontsLoaded) {
return null;
}
return <YourApp />;
}
The font packages handle remote font file hosting through Google's font CDN, ensuring reliable delivery and automatic updates when font files change on Google's servers. This delegation eliminates the need to download and manage font files manually for Google Fonts.
Creating Custom Font Packages
For organizations using proprietary fonts or fonts from sources outside Google Fonts, creating custom font packages follows similar patterns. Package structure involves creating a module that exports font loading functions and potentially sharing these packages across multiple projects.
Custom packages enable centralized font management, ensuring consistent typography across an organization's applications. Version control of font packages allows controlled rollouts of font updates and easy rollback when issues arise. When developing comprehensive mobile applications, integrating professional UI/UX design services alongside proper typography implementation creates cohesive brand experiences.
Performance Optimization Techniques
Optimizing font loading directly impacts application performance and user experience. Several techniques help minimize the performance impact of custom typography in React Native applications.
Font Subsetting
Font subsetting reduces file sizes by including only the characters your application actually uses. For applications targeting specific languages or use cases, removing unused glyphs can dramatically reduce font file sizes:
# Example using pyftsubset from fontTools
pyftsubset BrandFont.ttf \
--unicodes=U+0020-007F \
--layout-features='' \
--output-file=BrandFont-Latin.ttf
Preloading Critical Fonts
Preloading critical fonts before they're needed prevents loading delays during navigation:
import { useFonts, preloadFonts } from 'expo-font';
// Preload fonts before they're needed
useEffect(() => {
preloadFonts({
'BrandFont': require('./assets/fonts/BrandFont.ttf'),
'HeadingFont': require('./assets/fonts/HeadingFont.ttf'),
});
}, []);
// In navigation context, preload fonts for upcoming screens
const handleNavigationFocus = () => {
preloadFonts({
'DetailFont': require('./assets/fonts/DetailFont.ttf'),
});
};
Caching Strategies
Caching loaded fonts prevents redundant network requests:
import { useFonts } from 'expo-font';
import * as Font from 'expo-font';
// Custom hook with caching
function useCachedFonts(fontMap) {
const [fontsLoaded, setFontsLoaded] = useState(false);
useEffect(() => {
const loadFonts = async () => {
try {
// Check cache first
const cached = await Font.useCachedFonts(fontMap);
if (cached) {
setFontsLoaded(true);
return;
}
// Load and cache
await Font.loadAsync(fontMap);
setFontsLoaded(true);
} catch (e) {
console.error('Font loading failed:', e);
}
};
loadFonts();
}, [fontMap]);
return fontsLoaded;
}
Lazy Loading Non-Critical Fonts
Lazy loading defers non-critical fonts to improve startup performance:
import { useFonts } from 'expo-font';
import { useCallback } from 'react';
function useLazyFont(fontName, fontSource) {
const [shouldLoad, setShouldLoad] = useState(false);
const [fontsLoaded] = useFonts(shouldLoad ? { [fontName]: fontSource } : {});
const loadFont = useCallback(() => setShouldLoad(true), []);
return [fontsLoaded, loadFont];
}
// Usage for decorative or secondary screen fonts
const [headingFontLoaded, loadHeadingFont] = useLazyFont(
'DecorativeFont',
require('./assets/fonts/DecorativeFont.ttf')
);
Platform-Specific Considerations
Understanding platform differences ensures consistent font rendering across iOS, Android, and web targets.
iOS
iOS maintains a comprehensive font registry that automatically recognizes installed fonts. When embedding fonts through config plugin, iOS reads font family names directly from font metadata, potentially differing from filenames. Always verify font family names using iOS font inspection tools or documentation:
// On iOS, fontFamily must match the actual name in font metadata
export default function TextStyles() {
return (
<Text style={{
fontFamily: 'SF Pro Display', // Actual font metadata name
fontWeight: '600'
}}>
Heading Text
</Text>
);
}
Android
Android's font handling differs from iOS in several ways. The expo-font config plugin creates native XML font resources for Android when weight and style properties are specified, enabling proper integration with Android's text rendering pipeline. Without explicit properties, Android uses filenames as font family names:
// Android configuration with explicit weight and style
const fontConfig = {
fontFamily: 'BrandFont',
android: { weight: 400, style: 'normal' },
paths: ['./assets/fonts/BrandFont-Regular.ttf']
};
Web Targets
Web targets in React Native require different considerations. Font loading in web contexts follows web standards, with expo-font supporting both local file references and remote URL loading:
// Web-optimized font loading
const webFontConfig = {
'BrandFont': {
uri: 'https://cdn.example.com/fonts/BrandFont-Regular.woff2',
display: 'swap', // Control font display behavior
fonts: ['latin']
}
};
Cross-origin resource sharing policies affect remote font loading, requiring appropriate server configuration or using fonts that support open access.
Troubleshooting Common Issues
Font integration issues typically manifest as missing fonts, incorrect font rendering, or application crashes. Systematic troubleshooting identifies and resolves these problems efficiently.
Font Family Name Mismatches
Font family name mismatches represent the most common issue. JavaScript code referencing fontFamily must exactly match the registered font family name, which may differ from the font filename. Use platform-specific font inspection tools to verify actual font family names embedded in your application.
Verification Checklist:
- Verify font file integrity: Confirm the font file is not corrupted and opens correctly in font viewer applications
- Check font family name: Use font inspection tools to extract the actual font family name from the file metadata
- Match configuration names: Ensure config plugin settings and JavaScript fontFamily references match exactly
- Test on each platform: Font names may behave differently across iOS, Android, and web
Build Configuration Errors
Build configuration errors occur when font file paths are incorrect or file permissions prevent access:
# Verify font files exist at specified paths
ls -la assets/fonts/
# Clean and rebuild after adding fonts
expo prebuild --platform ios
expo prebuild --platform android
# Clear cache if fonts still don't appear
expo r -c
Memory Issues with Multiple Fonts
Memory issues can arise when loading many large fonts simultaneously:
// Sequential loading for memory-constrained scenarios
async function loadFontsSequentially(fontList) {
for (const font of fontList) {
await Font.loadAsync(font.source);
console.log(`Loaded: ${font.name}`);
}
}
// Usage
const fonts = [
{ name: 'Primary', source: require('./fonts/Primary.ttf') },
{ name: 'Secondary', source: require('./fonts/Secondary.ttf') },
{ name: 'Accent', source: require('./fonts/Accent.ttf') },
];
await loadFontsSequentially(fonts);
Final Verification Steps
After implementing custom fonts, verify successful integration across all target platforms:
- Test font rendering on physical devices (not just emulators)
- Verify fonts appear immediately without fallback flashes
- Check rendering at various font sizes for consistent quality
- Test under poor network conditions for runtime-loaded fonts
- Validate font weights and styles render correctly
For teams building cross-platform mobile applications, professional mobile app development services provide expertise in implementing consistent typography and brand identity across all platforms.
Frequently Asked Questions
What font formats does React Native support?
React Native and Expo support OTF and TTF formats across all platforms. iOS additionally supports WOFF and WOFF2 formats. For maximum compatibility, OTF is recommended when available, as it typically has smaller file sizes and better rendering quality.
Should I use the config plugin or useFonts hook?
Use the config plugin for production apps requiring immediate font availability and no loading states. This approach bundles fonts in the app, eliminating flash of unstyled text. Use useFonts when bundle size is critical or when loading fonts dynamically based on user behavior or preferences.
Do custom fonts work in Expo Go?
No, custom fonts embedded via config plugin require a development build. Expo Go supports runtime-loaded fonts through useFonts but not embedded fonts. For testing embedded fonts, create a development build using expo-dev-client.
How do I debug font loading issues?
Verify font family names match exactly between your code and font metadata. Check that font files are included in build outputs and that paths are correct in your configuration. Use platform inspection tools to verify fonts are properly registered on each platform.
Sources
- Expo Documentation - Fonts - Official documentation covering local font files, Google Font packages, config plugins, and useFonts hook
- LogRocket - How to add custom fonts in React Native - Comprehensive guide covering Expo and React Native CLI approaches, Google Fonts integration, and best practices