Dynamic Type & Font Scaling
Respecting system text size without layout breaks. A cookbook for allowFontScaling, maxFontSizeMultiplier, and layout patterns that survive iOS Dynamic Type and Android font size settings.
Search across all documentation pages
Respecting system text size without layout breaks. A cookbook for allowFontScaling, maxFontSizeMultiplier, and layout patterns that survive iOS Dynamic Type and Android font size settings.
Quick-reference recipe card - copy-paste ready.
import type { TextProps } from "react-native";
import { PixelRatio, StyleSheet, Text, View } from "react-native";
type AppTextProps = TextProps & {
variant?: "body" | "caption" | "toolbar";
};
const SCALE_CAPS = {
body: undefined,
caption: 1.5,
toolbar: 1.2,
} as const;
export function AppText({ variant = "body", style, ...rest }: AppTextProps) {
const cap = SCALE_CAPS[variant];
return (
<Text
allowFontScaling
maxFontSizeMultiplier={cap}
style={[typography[variant], style]}
{...rest}
/>
);
}
const typography = StyleSheet.create({
body: { fontSize: 16, lineHeight: 24 },
caption: { fontSize: 13, lineHeight: 18 },
toolbar: { fontSize: 14, lineHeight: 18, fontWeight: "600" },
});
export function useFontScale() {
return PixelRatio.getFontScale();
}
export function ScalableRow({ title, meta }: { title: string; meta: string }) {
return (
<View style={styles.row}>
<AppText variant="body" style={styles.title}>
{title}
</AppText>
<AppText variant="caption" style={styles.meta}>
{meta}
</AppText>
</View>
);
}
const styles = StyleSheet.create({
row: { flexDirection: "row", flexWrap: "wrap", gap: 8, paddingVertical: 12, alignItems: "center" },
title: { flexShrink: 1 },
meta: { color: "#64748b" },
});When to reach for this: Any screen with fixed-height toolbars, chips, bottom tabs, or multi-column rows - users with large text settings will clip or overlap your UI unless you plan for scale.
Settings list that reflows from single-line to multi-line when font scale exceeds a threshold.
import { useMemo } from "react";
import { PixelRatio, Pressable, StyleSheet, Text, View } from "react-native";
const LARGE_SCALE_THRESHOLD = 1.3;
export function SettingsScreen() {
const fontScale = PixelRatio.getFontScale();
const stacked = useMemo(() => fontScale >= LARGE_SCALE_THRESHOLD, [fontScale]);
return (
<View style={styles.screen}>
<View style={[styles.toolbar, stacked && styles.toolbarStacked]}>
<Text
style={styles.toolbarTitle}
maxFontSizeMultiplier={1.2}
numberOfLines={stacked ? 2 : 1}
>
Settings
</Text>
</View>
<Pressable style={[styles.row, stacked && styles.rowStacked]}>
<Text style={styles.rowTitle} allowFontScaling>
Notifications
</Text>
<Text style={styles.rowValue} maxFontSizeMultiplier={1.4}>
Enabled
</Text>
</Pressable>
<Pressable style={[styles.row, stacked && styles.rowStacked]}>
<Text style={styles.rowTitle} allowFontScaling>
Language
</Text>
<Text style={styles.rowValue} maxFontSizeMultiplier={1.4}>
English
</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16, gap: 8 },
toolbar: { minHeight: 44, justifyContent: "center" },
toolbarStacked: { minHeight: 56, paddingVertical: 8 },
toolbarTitle: { fontSize: 20, fontWeight: "700" },
row: {
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
minHeight: 48,
paddingVertical: 8,
gap: 12,
},
rowStacked: { flexDirection: "column", alignItems: "flex-start" },
rowTitle: { fontSize: 16, fontWeight: "600", flexShrink: 1 },
rowValue: { fontSize: 14, color: "#64748b" },
});What this demonstrates:
PixelRatio.getFontScale() drives layout mode switches at extreme scalesmaxFontSizeMultiplier on toolbar vs uncapped body textflexWrap / column reflow instead of numberOfLines={1} truncation on primary labelsminHeight on rows preserves touch targets when text wraps| UI region | allowFontScaling | maxFontSizeMultiplier | Layout note |
|---|---|---|---|
| Body paragraphs | true | none (uncapped) | Let text wrap; avoid fixed heights |
| Buttons (primary) | true | 1.3–1.5 | minHeight: 44, vertical padding |
| Tab bar labels | true | 1.1–1.2 | Short labels; allow two lines if needed |
| Chips / badges | true | 1.1 | flexWrap on chip rows |
| Data table headers | true | 1.2 | Switch to card layout at high scale |
| Decorative icons | N/A | N/A | Icon size fixed; label carries meaning |
Centralize typography tokens - see Typography Scale & Font Loading and Design Systems Basics.
// shared/ui/AppText.tsx - single scaling policy
export const typography = StyleSheet.create({
display: { fontSize: 34, lineHeight: 40 },
title: { fontSize: 22, lineHeight: 28 },
body: { fontSize: 16, lineHeight: 24 },
caption: { fontSize: 13, lineHeight: 18 },
});Text in features/** via ESLint - force AppText variants// Fragile - fixed height clips large text
<View style={{ height: 40 }}>
<Text numberOfLines={1}>Account settings and privacy controls</Text>
</View>
// Resilient - grows with content
<View style={{ minHeight: 44, paddingVertical: 8 }}>
<Text style={{ flexShrink: 1 }}>Account settings and privacy controls</Text>
</View>| Anti-pattern | Fix |
|---|---|
height: 32 on labels | minHeight + padding |
numberOfLines={1} on titles | Wrap or stack layout |
Horizontal row without flexShrink | flexShrink: 1 on text side |
| Absolute-positioned badges over text | Move badge below title at high scale |
<Text
adjustsFontSizeToFit
numberOfLines={1}
minimumFontScale={0.85}
maxFontSizeMultiplier={1.2}
style={styles.tabLabel}
>
Dashboard
</Text>adjustsFontSizeToFit - do not rely on it cross-platformFont scale can change while the app is backgrounded (user adjusts system settings).
import { useEffect, useState } from "react";
import { PixelRatio, AppState } from "react-native";
export function useFontScale() {
const [scale, setScale] = useState(PixelRatio.getFontScale());
useEffect(() => {
const sub = AppState.addEventListener("change", (state) => {
if (state === "active") {
setScale(PixelRatio.getFontScale());
}
});
return () => sub.remove();
}, []);
return scale;
}// Anti-pattern - rejects accessibility settings
<Text allowFontScaling={false}>Terms and conditions</Text>
// Prefer selective cap on dense chrome only
<Text maxFontSizeMultiplier={1.15} style={styles.badge}>
PRO
</Text>Disabling scaling app-wide is a store-review and procurement risk - cap selectively instead.
| Setting path | Effect in RN |
|---|---|
| iOS Display & Text Size → Larger Text | Scales Text when allowFontScaling is true |
| Android Display → Font size | Same via PixelRatio.getFontScale() |
| Per-app text size (iOS 15+) | Still flows through font scale APIs |
lineHeight should scale with fontSize - use token pairs (16/24, 22/28), not arbitrary single values.
1. iOS: Settings → Accessibility → Display & Text Size → Larger Text → maximum
2. Android: Settings → Display → Font size → largest
3. Walk onboarding, form, tab bar, and checkout
4. Verify: no clipped CTAs, no overlapping tab icons, no truncated legal text
5. Screenshot failures for design review - not just engineering ticketsLarge text and screen readers often overlap - users may enable both. Layouts that survive font scaling usually help zoom and reader users too. Announce layout changes sparingly; reflow alone does not need announceForAccessibility.
allowFontScaling={false} on TextInput - users cannot enlarge field text; avoid unless duplicating preview text nearby.1.1–1.2.1.3–1.5.AppState active or dimension changes.setInterval - event-driven refresh is enough.AppText primitive ownershipStack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026