Text Size
People pick a text size once, in Settings, and expect every app to follow. On iPhone, body text runs from 14 pt to 53 pt; Android goes to 200%. A screen built with fixed heights clips, truncates or prints labels over values, and it does that for the people who need large text most.
Drag the size up in each layout.
Your plan renews on November 3.
Drag the text size up to AX5 in each layout. Scroll the screen once it overflows.
Fixed rows keep their 44 pt height, so text is cut off and the value slides over its label. Rows that grow keep 44 pt as a minimum, wrap the long email, and from AX1 up put the value under its label.
Use the platform's text styles, give text containers minimum heights instead of fixed ones, and stack side-by-side content at the largest sizes.
How big it gets
iOS has 7 standard sizes and 5 accessibility sizes; Large is the default. Body text is 17 pt at Large, 14 pt at Extra Small and 23 pt at XXX Large, then 28, 33, 40, 47 and 53 pt from AX1 to AX5.
Big text grows less than small text, on purpose. Android 14 made the same choice with a nonlinear curve:
| Text | iOS, Large → AX5 | Android, 100% → 200% | |---|---|---| | Body | 17 → 53 pt (3.1×) | 14 → 26 sp (1.9×) | | Heading | 34 → 60 pt (1.8×) | 30 → 38 sp (1.3×) | | Display | | 100 → 100 sp |
So hierarchy flattens at the top end. At AX5 a large title is barely bigger than body text, and weight has to carry the difference. Don't shrink headings to compensate; the system has already held them back. For the scale itself, see Type Scale.
iPhone
- Use text styles:
.font(.body), never.font(.system(size: 15)), which doesn't scale. Custom fonts scale withFont.custom(_:size:relativeTo:). @ScaledMetric(relativeTo: .body)grows icon sizes and spacing that sit beside text.- Switch layouts with
dynamicTypeSize.isAccessibilitySizeandAnyLayout, or letViewThatFitspick the first one that fits. - Don't truncate. Apple: "aim to display as much useful text at the largest
accessibility font size as you do at the largest standard font size." Keep
lineLimit(1)off body text, and nothing below 11 pt. - Not everything grows. Tab bar labels stay put; add the Large Content Viewer
(
accessibilityShowsLargeContentViewer()) to controls like them.
Android
- Text and line height in
sp. Line height indpdoesn't scale, so large text looks cramped. - Never compute with
Configuration.fontScaleorscaledDensity; with a nonlinear curve one multiplier is wrong. Convert withTypedValue.applyDimension(). heightIn(min = 56.dp), notheight(56.dp), on rows, chips and buttons.- Test at 200%.
@PreviewFontScalepreviews a range of scales up to 2.0.
React Native
React Native ignores Apple's ramp by default. It multiplies every size by one factor for the setting, 3.571 at AX5, so a 34 pt title asks for 121 pt.
Travel · 4 min read
Weekend in Lisbon
Three days of tiled streets, custard tarts and the 28 tram up the hill.
Title 121 ptBody 61 ptCaption 43 pt
At AX5, compare one multiplier for every size with Apple's ramp for each text style.
Set dynamicTypeRamp on a Text and iOS scales it on Apple's curve for that
style instead. On both platforms, cap display text and fixed-height chrome
like tab labels with maxFontSizeMultiplier, and read
useWindowDimensions().fontScale to stack rows. Never set
allowFontScaling={false} on body text. It fails WCAG 1.4.4, which asks for
text that can grow to 200% without losing content.
Usage
struct AccountRow: View {
let label: String
let value: String
@Environment(\.dynamicTypeSize) private var typeSize
@ScaledMetric(relativeTo: .body) private var gap = 12 // grows with Body
var body: some View {
let layout = typeSize.isAccessibilitySize
? AnyLayout(VStackLayout(alignment: .leading, spacing: 4))
: AnyLayout(HStackLayout(alignment: .firstTextBaseline, spacing: gap))
layout {
Text(label)
if !typeSize.isAccessibilitySize { Spacer() }
Text(value).foregroundStyle(.secondary) // no lineLimit: let it wrap
}
.font(.body)
}
}
// Check it at the largest size in a preview.
#Preview { AccountRow(label: "Email", value: "alex.morgan@icloud.com")
.dynamicTypeSize(.accessibility5) }@Composable
fun AccountRow(label: String, value: String) {
// fontScale is fine for picking a layout, never for computing sizes.
val stacked = LocalDensity.current.fontScale >= 1.5f
val modifier = Modifier.fillMaxWidth()
.heightIn(min = 56.dp) // min, never height()
.padding(horizontal = 16.dp, vertical = 8.dp)
if (stacked) {
Column(modifier) {
Text(label, style = MaterialTheme.typography.bodyLarge)
Text(value, style = MaterialTheme.typography.bodyMedium)
}
} else {
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
Text(label, style = MaterialTheme.typography.bodyLarge)
Spacer(Modifier.width(16.dp))
Text(value, Modifier.weight(1f), textAlign = TextAlign.End,
style = MaterialTheme.typography.bodyMedium)
}
}
}
@PreviewFontScale
@Composable
fun AccountRowPreview() { AppTheme { AccountRow("Email", "alex.morgan@icloud.com") } }import { Text, View, useWindowDimensions } from "react-native";
function AccountRow({ label, value }: { label: string; value: string }) {
const { fontScale } = useWindowDimensions();
const stacked = fontScale >= 1.35; // about iOS XXX Large: stack, don't truncate
return (
<View style={{ minHeight: 44, paddingVertical: 10, gap: stacked ? 2 : 12,
flexDirection: stacked ? "column" : "row", justifyContent: "space-between" }}>
<Text style={styles.body}>{label}</Text>
<Text style={[styles.body, styles.secondary]}>{value}</Text>
</View>
);
}
// iOS: Apple's curve for this style instead of one multiplier.
<Text style={styles.largeTitle} dynamicTypeRamp="largeTitle">Account</Text>
// Cap chrome that can't grow forever. Never cap body text.
<Text style={styles.tabLabel} maxFontSizeMultiplier={1.3}>Home</Text>// TextScaler follows Android 14's nonlinear curve; textScaleFactor doesn't.
final scaler = MediaQuery.textScalerOf(context);
final stacked = scaler.scale(17) / 17 >= 1.35;
final row = Flex(
direction: stacked ? Axis.vertical : Axis.horizontal,
crossAxisAlignment: stacked ? CrossAxisAlignment.start : CrossAxisAlignment.center,
children: [
Text(label),
if (!stacked) const SizedBox(width: 16),
Flexible(child: Text(value, textAlign: stacked ? TextAlign.start : TextAlign.end)),
],
);
// Cap fixed-height chrome; never clamp body text.
MediaQuery.withClampedTextScaling(
maxScaleFactor: 1.3,
child: NavigationBar(destinations: destinations),
);Test every screen at AX5 and at 200% before you ship, with real content: the longest name, the longest translation, an email with no break points. A screen that scrolls at AX5 is fine. One that pushes the Save button out of reach, or cuts a price in half, is broken.
Resources
Human Interface Guidelines: Typography - Every iOS text style at all 12 Dynamic Type sizes, plus the 17 pt default and 11 pt minimum.
Get started with Dynamic Type - WWDC24 on text styles, ScaledMetric and stacking layouts at accessibility sizes.
Nonlinear font scaling to 200% - Android 14's curve, why scaledDensity is wrong now, and why line height belongs in sp.
Text (React Native) - allowFontScaling, maxFontSizeMultiplier and dynamicTypeRamp.
Understanding 1.4.4: Resize Text - The 200% rule, written for the web and the usual bar for native apps too.