Dynamic Type Reference
Use when implementing Dynamic Type for text views, scaling custom fonts, handling accessibility sizes, responding to content size category changes, or diagnosing what breaks at large sizes. Reach for this when the problem is text scaling and accessibility sizing, not VoiceOver or general accessibility.
Use when implementing Dynamic Type for text views, scaling custom fonts, handling accessibility sizes, responding to content size category changes, or diagnosing what breaks at large sizes. Reach for this when the problem is text scaling and accessibility sizing, not VoiceOver or general accessibility.
Family: Editor Features And Interaction
Use this skill when the main question is how text should scale with content size category and accessibility sizes.
When to Use
Section titled “When to Use”- You are implementing Dynamic Type in UIKit, AppKit, or SwiftUI.
- You are scaling custom fonts.
- You are testing layout behavior at large accessibility sizes.
Quick Decision
Section titled “Quick Decision”- Native text styles are enough -> use semantic text styles directly
- Custom font but standard scaling -> use
UIFontMetrics - Rich text or attributed text does not update -> handle size-category changes explicitly
Core Guidance
Section titled “Core Guidance”Text Styles and Default Sizes (at Large / Default)
Section titled “Text Styles and Default Sizes (at Large / Default)”| Text Style | UIKit | SwiftUI | Weight | Default Size |
|---|---|---|---|---|
| Extra Large Title 2 | .extraLargeTitle2 | .extraLargeTitle2 | Bold | 28pt |
| Extra Large Title | .extraLargeTitle | .extraLargeTitle | Bold | 36pt |
| Large Title | .largeTitle | .largeTitle | Regular | 34pt |
| Title 1 | .title1 | .title | Regular | 28pt |
| Title 2 | .title2 | .title2 | Regular | 22pt |
| Title 3 | .title3 | .title3 | Regular | 20pt |
| Headline | .headline | .headline | Semibold | 17pt |
| Body | .body | .body | Regular | 17pt |
| Callout | .callout | .callout | Regular | 16pt |
| Subheadline | .subheadline | .subheadline | Regular | 15pt |
| Footnote | .footnote | .footnote | Regular | 13pt |
| Caption 1 | .caption1 | .caption | Regular | 12pt |
| Caption 2 | .caption2 | .caption2 | Regular | 11pt |
Point Size Scaling Table (Body Style)
Section titled “Point Size Scaling Table (Body Style)”| Category | Body Size | API Constant |
|---|---|---|
| xSmall | 14pt | .extraSmall |
| Small | 15pt | .small |
| Medium | 16pt | .medium |
| Large (Default) | 17pt | .large |
| xLarge | 19pt | .extraLarge |
| xxLarge | 21pt | .extraExtraLarge |
| xxxLarge | 23pt | .extraExtraExtraLarge |
| AX1 | 28pt | .accessibilityMedium |
| AX2 | 33pt | .accessibilityLarge |
| AX3 | 40pt | .accessibilityExtraLarge |
| AX4 | 47pt | .accessibilityExtraExtraLarge |
| AX5 | 53pt | .accessibilityExtraExtraExtraLarge |
At AX5, Body text is 3x its default size.
What Automatically Supports Dynamic Type
Section titled “What Automatically Supports Dynamic Type”| Component | Auto-scales? | Notes |
|---|---|---|
SwiftUI Text with .font(.body) | ✅ | All semantic font styles scale |
SwiftUI Text with .font(.system(size: 17)) | ❌ | Fixed size — does NOT scale |
SwiftUI Text with .font(.custom("X", size: 17, relativeTo: .body)) | ✅ | Scales via relativeTo: |
SwiftUI Text with .font(.custom("X", fixedSize: 17)) | ❌ | Fixed — does NOT scale |
UILabel with preferredFont(forTextStyle:) | ✅ if adjustsFontForContentSizeCategory = true | |
UILabel with UIFont.systemFont(ofSize: 17) | ❌ | Fixed size |
UITextView with preferredFont(forTextStyle:) | ✅ if adjustsFontForContentSizeCategory = true | |
NSAttributedString with fixed font | ❌ | Must re-apply fonts on size change |
NSTextView | Partial | macOS Dynamic Type is limited |
UIKit: Making Text Scale
Section titled “UIKit: Making Text Scale”System Fonts
Section titled “System Fonts”label.font = UIFont.preferredFont(forTextStyle: .body)label.adjustsFontForContentSizeCategory = true // Auto-update on changeWithout adjustsFontForContentSizeCategory: The font is a snapshot — it doesn’t update when the user changes their size preference.
Custom Fonts with UIFontMetrics
Section titled “Custom Fonts with UIFontMetrics”let customFont = UIFont(name: "Avenir-Medium", size: 17)!let metrics = UIFontMetrics(forTextStyle: .body)
// Scale with no upper boundlabel.font = metrics.scaledFont(for: customFont)
// Scale with maximumlabel.font = metrics.scaledFont(for: customFont, maximumPointSize: 28)
// Scale non-font values (padding, spacing, icon sizes)let scaledPadding = metrics.scaledValue(for: 16.0)
label.adjustsFontForContentSizeCategory = trueThe base size (17 in this example) should be the default (Large) size.
UITextView
Section titled “UITextView”textView.font = UIFont.preferredFont(forTextStyle: .body)textView.adjustsFontForContentSizeCategory = trueWith attributed text: adjustsFontForContentSizeCategory does NOT re-scale attributed string fonts. You must listen for size changes and re-apply fonts:
NotificationCenter.default.addObserver( forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: .main) { _ in self.reapplyDynamicFonts()}Limiting Scale Range (iOS 15+)
Section titled “Limiting Scale Range (iOS 15+)”// Prevent text from getting too small or too largeview.minimumContentSizeCategory = .mediumview.maximumContentSizeCategory = .accessibilityLargeWorks on any UIView. Caps the effective content size category for that view and its children.
SwiftUI: Making Text Scale
Section titled “SwiftUI: Making Text Scale”Semantic Fonts (Always Scale)
Section titled “Semantic Fonts (Always Scale)”Text("Scales").font(.body) // ✅ ScalesText("Scales").font(.headline) // ✅ ScalesText("Scales").font(.largeTitle) // ✅ ScalesCustom Fonts
Section titled “Custom Fonts”// ✅ Scales with Dynamic TypeText("Custom").font(.custom("Avenir", size: 17, relativeTo: .body))
// ❌ Does NOT scaleText("Fixed").font(.custom("Avenir", fixedSize: 17))Text("Fixed").font(.system(size: 17))@ScaledMetric for Non-Font Values
Section titled “@ScaledMetric for Non-Font Values”@ScaledMetric(relativeTo: .body) var iconSize: CGFloat = 24@ScaledMetric var padding: CGFloat = 16 // Uses .body curve by default
Image(systemName: "star") .frame(width: iconSize, height: iconSize) .padding(padding)Limiting Scale Range (iOS 15+)
Section titled “Limiting Scale Range (iOS 15+)”Text("Limited") .dynamicTypeSize(.medium ... .accessibility3)NSFont.preferredFont(forTextStyle:) exists but macOS Dynamic Type is more limited:
- Only available on macOS 11+
- macOS doesn’t have the same accessibility size categories
- Users change text size via System Settings → Accessibility → Display → Text Size (macOS 14+)
- SwiftUI on macOS uses the same
.font(.body)API and it scales
Non-Latin Script Line Height (iOS 17+)
Section titled “Non-Latin Script Line Height (iOS 17+)”iOS 17 introduced automatic dynamic line-height adjustment for scripts that are taller than Latin (Thai, Arabic, Devanagari, Tibetan). Previously, fixed minimumLineHeight/maximumLineHeight values could clip ascenders and descenders on these scripts. The system now adjusts line height per-line based on the actual glyphs rendered.
If you set explicit minimumLineHeight/maximumLineHeight on NSParagraphStyle, the system respects your values and may still clip. For multilingual content, prefer lineHeightMultiple over fixed heights, or avoid constraining line height entirely.
What Breaks at Large Accessibility Sizes
Section titled “What Breaks at Large Accessibility Sizes”| Issue | Solution |
|---|---|
| Text clips in fixed-height containers | Use adjustsFontSizeToFitWidth or flexible layouts |
| Horizontal layouts overflow | Switch to vertical at accessibility sizes |
| Icons too small relative to text | Use @ScaledMetric / UIFontMetrics.scaledValue(for:) |
| Table cells too short | Use self-sizing cells with Auto Layout |
| Navigation bar titles truncate | System handles, but custom title views need attention |
| Buttons with text overflow | Allow multi-line button labels |
Large Content Viewer (iOS 13+)
Section titled “Large Content Viewer (iOS 13+)”For UI elements that can’t grow (tab bars, toolbars, segmented controls):
// UIKitbutton.showsLargeContentViewer = truebutton.largeContentTitle = "Settings"button.largeContentImage = UIImage(systemName: "gear")
// SwiftUIButton("Settings") { } .accessibilityShowsLargeContentViewer { Label("Settings", systemImage: "gear") }Long-press shows a HUD with the enlarged content.
Testing Dynamic Type
Section titled “Testing Dynamic Type”- Xcode Environment Overrides: Run app → Debug bar → Environment Overrides → Text Size slider
- Control Center: Settings → Control Center → Add “Text Size” → Adjust while app runs
- Accessibility Inspector: Xcode → Open Developer Tool → Accessibility Inspector → Settings → Font Size
- SwiftUI Preview:
.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)
Common Pitfalls
Section titled “Common Pitfalls”- Fixed font sizes don’t scale —
.system(size: 17)andUIFont.systemFont(ofSize: 17)are permanently 17pt. Use semantic styles. - Attributed strings don’t auto-scale —
adjustsFontForContentSizeCategoryonly works with plainfontproperty. For attributed text, listen forUIContentSizeCategory.didChangeNotification. - Forgetting
adjustsFontForContentSizeCategory— Without it, UIKit text views get a snapshot font that never updates. - Not testing accessibility sizes — AX3-AX5 are where most layout bugs appear. Always test.
- Icons not scaling — SF Symbols auto-scale with Dynamic Type. Custom icons need
@ScaledMetricorUIFontMetrics.scaledValue.
Documentation Scope
Section titled “Documentation Scope”This page documents the apple-text-dynamic-type reference skill. Use it when the subsystem is already known and you need mechanics, behavior, or API detail.
Related
Section titled “Related”apple-text-views: Use when the main task is choosing the right Apple text view or deciding whether a problem belongs in SwiftUI text, UIKit/AppKit text views, or TextKit mode. Reach for this when comparing capabilities and tradeoffs, not when implementing a specific wrapper or low-level API.apple-text-colors: Use when choosing text colors, semantic colors, dark-mode behavior, wide-color or HDR text, or making attributed-text colors adapt correctly across UIKit, AppKit, and SwiftUI. Reach for this when the problem is specifically text color behavior, not broader formatting or typography.apple-text-writing-tools: Use when integrating Writing Tools into a native or custom text editor, configuring writingToolsBehavior, adopting UIWritingToolsCoordinator, protecting ranges, or debugging why Writing Tools do not appear. Reach for this when the problem is specifically Writing Tools, not generic editor debugging.
Full SKILL.md source
---name: apple-text-dynamic-typedescription: Use when implementing Dynamic Type for text views, scaling custom fonts, handling accessibility sizes, responding to content size category changes, or diagnosing what breaks at large sizes. Reach for this when the problem is text scaling and accessibility sizing, not VoiceOver or general accessibility.license: MIT---
# Dynamic Type Reference
Use this skill when the main question is how text should scale with content size category and accessibility sizes.
## When to Use
- You are implementing Dynamic Type in UIKit, AppKit, or SwiftUI.- You are scaling custom fonts.- You are testing layout behavior at large accessibility sizes.
## Quick Decision
- Native text styles are enough -> use semantic text styles directly- Custom font but standard scaling -> use `UIFontMetrics`- Rich text or attributed text does not update -> handle size-category changes explicitly
## Core Guidance
## Text Styles and Default Sizes (at Large / Default)
| Text Style | UIKit | SwiftUI | Weight | Default Size ||------------|-------|---------|--------|-------------|| Extra Large Title 2 | `.extraLargeTitle2` | `.extraLargeTitle2` | Bold | 28pt || Extra Large Title | `.extraLargeTitle` | `.extraLargeTitle` | Bold | 36pt || Large Title | `.largeTitle` | `.largeTitle` | Regular | 34pt || Title 1 | `.title1` | `.title` | Regular | 28pt || Title 2 | `.title2` | `.title2` | Regular | 22pt || Title 3 | `.title3` | `.title3` | Regular | 20pt || Headline | `.headline` | `.headline` | **Semibold** | 17pt || Body | `.body` | `.body` | Regular | 17pt || Callout | `.callout` | `.callout` | Regular | 16pt || Subheadline | `.subheadline` | `.subheadline` | Regular | 15pt || Footnote | `.footnote` | `.footnote` | Regular | 13pt || Caption 1 | `.caption1` | `.caption` | Regular | 12pt || Caption 2 | `.caption2` | `.caption2` | Regular | 11pt |
## Point Size Scaling Table (Body Style)
| Category | Body Size | API Constant ||----------|----------|-------------|| xSmall | 14pt | `.extraSmall` || Small | 15pt | `.small` || Medium | 16pt | `.medium` || **Large (Default)** | **17pt** | `.large` || xLarge | 19pt | `.extraLarge` || xxLarge | 21pt | `.extraExtraLarge` || xxxLarge | 23pt | `.extraExtraExtraLarge` || **AX1** | **28pt** | `.accessibilityMedium` || **AX2** | **33pt** | `.accessibilityLarge` || **AX3** | **40pt** | `.accessibilityExtraLarge` || **AX4** | **47pt** | `.accessibilityExtraExtraLarge` || **AX5** | **53pt** | `.accessibilityExtraExtraExtraLarge` |
At AX5, Body text is **3x** its default size.
## What Automatically Supports Dynamic Type
| Component | Auto-scales? | Notes ||-----------|-------------|-------|| SwiftUI `Text` with `.font(.body)` | ✅ | All semantic font styles scale || SwiftUI `Text` with `.font(.system(size: 17))` | ❌ | Fixed size — does NOT scale || SwiftUI `Text` with `.font(.custom("X", size: 17, relativeTo: .body))` | ✅ | Scales via `relativeTo:` || SwiftUI `Text` with `.font(.custom("X", fixedSize: 17))` | ❌ | Fixed — does NOT scale || `UILabel` with `preferredFont(forTextStyle:)` | ✅ if `adjustsFontForContentSizeCategory = true` || `UILabel` with `UIFont.systemFont(ofSize: 17)` | ❌ | Fixed size || `UITextView` with `preferredFont(forTextStyle:)` | ✅ if `adjustsFontForContentSizeCategory = true` || `NSAttributedString` with fixed font | ❌ | Must re-apply fonts on size change || `NSTextView` | Partial | macOS Dynamic Type is limited |
## UIKit: Making Text Scale
### System Fonts
```swiftlabel.font = UIFont.preferredFont(forTextStyle: .body)label.adjustsFontForContentSizeCategory = true // Auto-update on change```
**Without `adjustsFontForContentSizeCategory`:** The font is a snapshot — it doesn't update when the user changes their size preference.
### Custom Fonts with UIFontMetrics
```swiftlet customFont = UIFont(name: "Avenir-Medium", size: 17)!let metrics = UIFontMetrics(forTextStyle: .body)
// Scale with no upper boundlabel.font = metrics.scaledFont(for: customFont)
// Scale with maximumlabel.font = metrics.scaledFont(for: customFont, maximumPointSize: 28)
// Scale non-font values (padding, spacing, icon sizes)let scaledPadding = metrics.scaledValue(for: 16.0)
label.adjustsFontForContentSizeCategory = true```
The base size (17 in this example) should be the **default (Large)** size.
### UITextView
```swifttextView.font = UIFont.preferredFont(forTextStyle: .body)textView.adjustsFontForContentSizeCategory = true```
**With attributed text:** `adjustsFontForContentSizeCategory` does NOT re-scale attributed string fonts. You must listen for size changes and re-apply fonts:
```swiftNotificationCenter.default.addObserver( forName: UIContentSizeCategory.didChangeNotification, object: nil, queue: .main) { _ in self.reapplyDynamicFonts()}```
### Limiting Scale Range (iOS 15+)
```swift// Prevent text from getting too small or too largeview.minimumContentSizeCategory = .mediumview.maximumContentSizeCategory = .accessibilityLarge```
Works on any UIView. Caps the effective content size category for that view and its children.
## SwiftUI: Making Text Scale
### Semantic Fonts (Always Scale)
```swiftText("Scales").font(.body) // ✅ ScalesText("Scales").font(.headline) // ✅ ScalesText("Scales").font(.largeTitle) // ✅ Scales```
### Custom Fonts
```swift// ✅ Scales with Dynamic TypeText("Custom").font(.custom("Avenir", size: 17, relativeTo: .body))
// ❌ Does NOT scaleText("Fixed").font(.custom("Avenir", fixedSize: 17))Text("Fixed").font(.system(size: 17))```
### @ScaledMetric for Non-Font Values
```swift@ScaledMetric(relativeTo: .body) var iconSize: CGFloat = 24@ScaledMetric var padding: CGFloat = 16 // Uses .body curve by default
Image(systemName: "star") .frame(width: iconSize, height: iconSize) .padding(padding)```
### Limiting Scale Range (iOS 15+)
```swiftText("Limited") .dynamicTypeSize(.medium ... .accessibility3)```
## macOS
`NSFont.preferredFont(forTextStyle:)` exists but macOS Dynamic Type is more limited:- Only available on macOS 11+- macOS doesn't have the same accessibility size categories- Users change text size via System Settings → Accessibility → Display → Text Size (macOS 14+)- SwiftUI on macOS uses the same `.font(.body)` API and it scales
## Non-Latin Script Line Height (iOS 17+)
iOS 17 introduced automatic dynamic line-height adjustment for scripts that are taller than Latin (Thai, Arabic, Devanagari, Tibetan). Previously, fixed `minimumLineHeight`/`maximumLineHeight` values could clip ascenders and descenders on these scripts. The system now adjusts line height per-line based on the actual glyphs rendered.
**If you set explicit `minimumLineHeight`/`maximumLineHeight` on NSParagraphStyle**, the system respects your values and may still clip. For multilingual content, prefer `lineHeightMultiple` over fixed heights, or avoid constraining line height entirely.
## What Breaks at Large Accessibility Sizes
| Issue | Solution ||-------|---------|| Text clips in fixed-height containers | Use `adjustsFontSizeToFitWidth` or flexible layouts || Horizontal layouts overflow | Switch to vertical at accessibility sizes || Icons too small relative to text | Use `@ScaledMetric` / `UIFontMetrics.scaledValue(for:)` || Table cells too short | Use self-sizing cells with Auto Layout || Navigation bar titles truncate | System handles, but custom title views need attention || Buttons with text overflow | Allow multi-line button labels |
### Large Content Viewer (iOS 13+)
For UI elements that can't grow (tab bars, toolbars, segmented controls):
```swift// UIKitbutton.showsLargeContentViewer = truebutton.largeContentTitle = "Settings"button.largeContentImage = UIImage(systemName: "gear")
// SwiftUIButton("Settings") { } .accessibilityShowsLargeContentViewer { Label("Settings", systemImage: "gear") }```
Long-press shows a HUD with the enlarged content.
## Testing Dynamic Type
1. **Xcode Environment Overrides:** Run app → Debug bar → Environment Overrides → Text Size slider2. **Control Center:** Settings → Control Center → Add "Text Size" → Adjust while app runs3. **Accessibility Inspector:** Xcode → Open Developer Tool → Accessibility Inspector → Settings → Font Size4. **SwiftUI Preview:** `.environment(\.sizeCategory, .accessibilityExtraExtraExtraLarge)`
## Common Pitfalls
1. **Fixed font sizes don't scale** — `.system(size: 17)` and `UIFont.systemFont(ofSize: 17)` are permanently 17pt. Use semantic styles.2. **Attributed strings don't auto-scale** — `adjustsFontForContentSizeCategory` only works with plain `font` property. For attributed text, listen for `UIContentSizeCategory.didChangeNotification`.3. **Forgetting `adjustsFontForContentSizeCategory`** — Without it, UIKit text views get a snapshot font that never updates.4. **Not testing accessibility sizes** — AX3-AX5 are where most layout bugs appear. Always test.5. **Icons not scaling** — SF Symbols auto-scale with Dynamic Type. Custom icons need `@ScaledMetric` or `UIFontMetrics.scaledValue`.
## Related Skills
- Use `/skill apple-text-views` when sizing depends on which control you chose.- Use `/skill apple-text-colors` for contrast and semantic color pairing.- Use `/skill apple-text-writing-tools` when accessibility sizing affects editor integrations.