Text and Fonts
Text style props, truncation with numberOfLines, and loading a custom font with expo-font without a flash of the wrong one.
const [loaded, error] = useFonts({ 'Inter-Bold': require('./assets/fonts/Inter-Bold.ttf') })All text lives inside a Text component, and all text styling is set there. fontSize is a number of dp. fontWeight accepts 'normal', 'bold' or the numeric strings '100' through '900'; 'bold' and '700' are the same request, and the docs note that if a font has no variant for a numeric value, the closest one is chosen. lineHeight is an absolute number, the distance between baselines, not a multiplier like CSS allows.
Inheritance, but only here
This is the one place React Native inherits anything. A Text nested inside another Text picks up the outer text styles and overrides what it sets itself, which is how a bold run inside a sentence is written. Nothing crosses a View boundary.
Truncating
numberOfLines clips the text after layout and adds an ellipsis; 0 removes the limit. ellipsizeMode chooses where the ellipsis goes, defaulting to 'tail', and on Android only 'tail' works correctly once numberOfLines is above 1.
Custom fonts
In Expo, useFonts from expo-font returns a tuple: const [loaded, error] = useFonts(fontMap). Each file gets its own family name, and you reference that name in fontFamily. Until loaded is true the font is not available, so hold the splash screen with SplashScreen.preventAutoHideAsync() and hide it once loading settles, rather than rendering text in the system font and letting it jump. The expo-font config plugin embeds the files at build time instead, which the Expo docs call more efficient than loading at runtime.
Example
import { useEffect } from 'react';
import { useFonts } from 'expo-font';
import * as SplashScreen from 'expo-splash-screen';
import { StyleSheet, Text, View } from 'react-native';
// Keep the splash screen up until the fonts are ready.
SplashScreen.preventAutoHideAsync();
export default function TextStylingDemo() {
// Every weight is a separate file and a separate family name.
const [loaded, error] = useFonts({
'Inter-Regular': require('../assets/fonts/Inter-Regular.ttf'),
'Inter-Bold': require('../assets/fonts/Inter-Bold.ttf'),
});
// Hooks run before any early return, in the same order every render.
useEffect(() => {
if (loaded || error) {
SplashScreen.hideAsync();
}
}, [loaded, error]);
if (!loaded && !error) {
return null; // splash is still showing; nothing flashes in the system font
}
return (
<View style={styles.screen}>
<Text style={styles.heading}>Release notes</Text>
<Text style={styles.body} numberOfLines={2}>
This paragraph is cut off after the second line with a trailing
ellipsis, and a <Text style={styles.strong}>bold run</Text> inherits
everything except the family it overrides.
</Text>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16, gap: 8, backgroundColor: '#ffffff' },
heading: { fontFamily: 'Inter-Bold', fontSize: 24, lineHeight: 30 },
body: { fontFamily: 'Inter-Regular', fontSize: 16, lineHeight: 24 },
strong: { fontFamily: 'Inter-Bold' }, // fontWeight would not change the file
});When to use it
- Ship a brand font and show no text until it has loaded.
- Cut a description to two lines with an ellipsis in a card.
- Bold one phrase inside a sentence without splitting it into separate rows.
- Keep a fixed-height badge from clipping when the reader enlarges system text.
More examples
A type scale, with both spellings of bold
Four sizes of text in a column, each with its line height set explicitly rather than left to the font. The two headings are deliberately written with the two spellings of bold to show they are interchangeable on a system font. Keeping a scale like this in one file is what stops font sizes drifting across screens.
import { StyleSheet, Text, View } from 'react-native';
export default function TypeScale() {
return (
<View style={styles.screen}>
<Text style={styles.h1}>Account</Text>
<Text style={styles.h2}>Billing</Text>
<Text style={styles.body}>
Body copy at 16dp with a 24dp line height, so the leading stays even
across two or more lines.
</Text>
<Text style={styles.caption}>Updated 3 minutes ago</Text>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16, gap: 8 },
// fontWeight: 'bold' and fontWeight: '700' are the same request.
h1: { fontSize: 28, fontWeight: 'bold', lineHeight: 34 },
h2: { fontSize: 20, fontWeight: '700', lineHeight: 26 },
body: { fontSize: 16, lineHeight: 24, color: '#1c1c1e' },
caption: { fontSize: 13, color: '#636366', letterSpacing: 0.2 },
});Respecting, and capping, the user's text size
A rounded grey pill sized to its label, which grows with the reader's accessibility text setting but no further than 1.4 times the base size. Turning allowFontScaling off entirely would be an accessibility regression; capping the multiplier and using minHeight keeps the layout intact without ignoring the setting.
import { StyleSheet, Text, View } from 'react-native';
type PillProps = { label: string };
export default function Pill({ label }: PillProps) {
return (
<View style={styles.pill}>
{/* allowFontScaling is true by default: text grows with device settings.
maxFontSizeMultiplier caps it so a fixed-height pill cannot clip. */}
<Text
style={styles.label}
maxFontSizeMultiplier={1.4}
numberOfLines={1}
ellipsizeMode="tail"
>
{label}
</Text>
</View>
);
}
const styles = StyleSheet.create({
pill: {
alignSelf: 'flex-start',
minHeight: 32, // minHeight, not height, so large text still fits
justifyContent: 'center',
paddingHorizontal: 12,
borderRadius: 16,
backgroundColor: '#e5e5ea',
},
label: { fontSize: 14, color: '#1c1c1e' },
});
Discussion