Skip to content

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.

Account
NameAlex Morgan
Emailalex.morgan@icloud.com
Phone+1 (555) 010-0199
PlanFamily, 2 TB

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 with Font.custom(_:size:relativeTo:).
  • @ScaledMetric(relativeTo: .body) grows icon sizes and spacing that sit beside text.
  • Switch layouts with dynamicTypeSize.isAccessibilitySize and AnyLayout, or let ViewThatFits pick 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 in dp doesn't scale, so large text looks cramped.
  • Never compute with Configuration.fontScale or scaledDensity; with a nonlinear curve one multiplier is wrong. Convert with TypedValue.applyDimension().
  • heightIn(min = 56.dp), not height(56.dp), on rows, chips and buttons.
  • Test at 200%. @PreviewFontScale previews 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

ContentView.swift
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

Search concepts

Search for a command to run...