# Text Size

> Let text grow and layouts reflow.

- Section: Mobile
- URL: https://critly.vercel.app/text-size
- Published: 2026-10-12
- Source: https://github.com/kausthubh-coder/critly/blob/main/content/mobile/text-size.mdx

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.

> **Interactive demo: Text Size.** Open https://critly.vercel.app/text-size to try it.

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](https://critly.vercel.app/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.

> **Interactive demo: Text Reflow.** Open https://critly.vercel.app/text-size to try it.

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

**SwiftUI**

```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) }
```

**Compose**

```kotlin
@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") } }
```

**React Native**

```tsx
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>
```

**Flutter**

```dart
// 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](https://developer.apple.com/design/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](https://developer.apple.com/videos/play/wwdc2024/10074/): WWDC24 on text styles, ScaledMetric and stacking layouts at accessibility sizes.
- [Nonlinear font scaling to 200%](https://developer.android.com/about/versions/14/features#non-linear-font-scaling): Android 14's curve, why scaledDensity is wrong now, and why line height belongs in sp.
- [Text (React Native)](https://reactnative.dev/docs/text): allowFontScaling, maxFontSizeMultiplier and dynamicTypeRamp.
- [Understanding 1.4.4: Resize Text](https://www.w3.org/WAI/WCAG22/Understanding/resize-text.html): The 200% rule, written for the web and the usual bar for native apps too.
