How to Build Dynamic Type in SwiftUI
Use SwiftUI's built-in semantic Font type styles (e.g. .title, .body) and they scale automatically with the user's Dynamic Type preference — no extra work required. Read @Environment(\.dynamicTypeSize) to adapt layout for very large sizes.
struct QuickLabel: View {
@Environment(\.dynamicTypeSize) var typeSize
var body: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Headline")
.font(.headline)
Text("This body text scales automatically.")
.font(.body)
if typeSize >= .accessibility1 {
Text("(Large-size layout active)")
.font(.caption)
.foregroundStyle(.secondary)
}
}
.padding()
}
}
Full implementation
The example below builds a settings-style row that demonstrates three Dynamic Type patterns in one view: semantic font styles that scale for free, an environment-aware layout that switches from horizontal to vertical stacking at Accessibility sizes, and a scoped .dynamicTypeSize modifier on an icon to prevent it from growing too large. A realistic preview exercises the default, large, and accessibility-large sizes side-by-side.
import SwiftUI
// MARK: - Model
struct SettingsItem: Identifiable {
let id = UUID()
let icon: String
let iconColor: Color
let title: String
let subtitle: String
}
// MARK: - Row View
struct SettingsRow: View {
let item: SettingsItem
@Environment(\.dynamicTypeSize) private var typeSize
/// Switch to a vertical stack at accessibility sizes so
/// long text doesn't get crushed next to the icon.
private var isAccessibilitySize: Bool {
typeSize >= .accessibility1
}
var body: some View {
Group {
if isAccessibilitySize {
VStack(alignment: .leading, spacing: 8) {
iconView
textStack
}
} else {
HStack(spacing: 14) {
iconView
textStack
Spacer()
}
}
}
.padding(.vertical, 10)
.padding(.horizontal, 16)
.background(.background)
.clipShape(RoundedRectangle(cornerRadius: 12))
}
// MARK: Sub-views
private var iconView: some View {
Image(systemName: item.icon)
.font(.title2)
.foregroundStyle(.white)
.frame(width: 36, height: 36)
.background(item.iconColor, in: RoundedRectangle(cornerRadius: 8))
// Prevent the icon from inflating at accessibility sizes
.dynamicTypeSize(...DynamicTypeSize.xxxLarge)
.accessibilityHidden(true)
}
private var textStack: some View {
VStack(alignment: .leading, spacing: 2) {
Text(item.title)
.font(.body)
.fontWeight(.medium)
.foregroundStyle(.primary)
Text(item.subtitle)
.font(.subheadline)
.foregroundStyle(.secondary)
}
}
}
// MARK: - List View
struct SettingsList: View {
let items: [SettingsItem] = [
SettingsItem(icon: "bell.fill", iconColor: .red, title: "Notifications", subtitle: "Banners, sounds, badges"),
SettingsItem(icon: "lock.fill", iconColor: .blue, title: "Privacy", subtitle: "Permissions & data"),
SettingsItem(icon: "paintbrush.fill", iconColor: .purple, title: "Appearance", subtitle: "Theme & font size"),
]
var body: some View {
ScrollView {
VStack(spacing: 1) {
ForEach(items) { item in
SettingsRow(item: item)
Divider().padding(.leading, 66)
}
}
.background(.secondarySystemBackground)
.clipShape(RoundedRectangle(cornerRadius: 14))
.padding()
}
.background(Color(.systemGroupedBackground))
.navigationTitle("Settings")
}
}
// MARK: - Preview
#Preview("Default") {
NavigationStack { SettingsList() }
}
#Preview("Accessibility XL") {
NavigationStack { SettingsList() }
.environment(\.dynamicTypeSize, .accessibility3)
}
How it works
-
Semantic Font styles scale automatically.
.font(.body),.font(.subheadline), and every other built-in text style are mapped to the Apple Human Interface Guidelines type ramp. The system multiplies their point size by the user's chosen scale factor with zero code on your part. -
@Environment(\.dynamicTypeSize)exposes the current size category. TheisAccessibilitySizecomputed property compares it against.accessibility1(the first of five extra-large categories) and switches the row fromHStacktoVStackso text never wraps awkwardly beside the icon. -
.dynamicTypeSize(...DynamicTypeSize.xxxLarge)clamps the icon. The one-sided range operator pins the icon's own scaling ceiling atxxxLarge. The icon stays recognisably small even ataccessibility5, while surrounding text can still grow unconstrained. -
Layout branching keeps reading flow intact. At normal sizes the icon lives to the left in an
HStack. At accessibility sizes it moves above the text in aVStack, giving very large text the full row width to breathe. -
The icon is hidden from VoiceOver with
.accessibilityHidden(true). The SF Symbol is decorative — the row's title and subtitle already describe the action, so hiding the icon prevents screen-reader duplication.
Variants
Scaling a custom font
// Scale a custom font relative to a built-in text style.
// The system multiplies the custom font's size by the same
// ratio it applies to the chosen style (here .body).
struct ScaledCustomFont: View {
// Declare once so the scaled value is stable across redraws
@ScaledMetric(relativeTo: .body) private var fontSize: CGFloat = 17
var body: some View {
Text("Custom scaled text")
.font(.custom("Georgia", size: fontSize))
}
}
#Preview {
ScaledCustomFont()
.environment(\.dynamicTypeSize, .accessibility2)
}
Previewing multiple sizes at once
SwiftUI's #Preview macro lets you inject any environment value, so you can spin up a ForEach over the DynamicTypeSize allCases array to see every size category in Xcode's canvas without running on a device. Keep these previews in your source during development so layout regressions are caught immediately — Soarias automatically strips debug previews before release builds.
Common pitfalls
-
⚠️ Hardcoded point sizes bypass the type ramp. Writing
.font(.system(size: 16))without.bold()or arelativeTo:argument produces a fixed-size font that never scales. Always prefer semantic styles or@ScaledMetric. -
⚠️ Fixed-height containers clip large text. A
.frame(height: 44)row that looked fine at default size will clip text at Accessibility Large. Use.frame(minHeight: 44)so the container can grow, or switch to aVStack-based layout at accessibility sizes as shown above. -
⚠️ Missing accessibility labels on icon-only buttons. At the largest Dynamic Type settings, icon-only controls are especially risky: the symbol may look clear at default size but become ambiguous when stretched. Always add
.accessibilityLabel("Close")(or equivalent) to every unlabelled interactive element regardless of type size.
Prompt this with Claude Code
When using Soarias or Claude Code directly to implement this:
Implement dynamic type in SwiftUI for iOS 17+. Use Font and dynamicTypeSize. Make it accessible (VoiceOver labels). Add a #Preview with realistic sample data.
In Soarias's Build phase, paste this prompt into the active screen file to let Claude generate an accessible layout scaffold — then use the canvas preview to spot-check every Dynamic Type size category before moving on.
Related
FAQ
Does this work on iOS 16?
Semantic Font styles and @ScaledMetric work back to iOS 15. The DynamicTypeSize enum and \.dynamicTypeSize environment key were introduced in iOS 15 as well, so all patterns in this guide are available on iOS 16. The #Preview macro requires Xcode 15+ but the runtime behaviour is fully backward compatible.
How do I scale non-text elements like spacing and icon sizes?
Use @ScaledMetric for any CGFloat value you want to track the type ramp — padding, minimum row heights, icon frame sizes. Pass relativeTo: to anchor it to a specific text style: @ScaledMetric(relativeTo: .body) var rowHeight: CGFloat = 44. The property wrapper automatically multiplies the base value by the same factor the system applies to the chosen text style.
What is the UIKit equivalent?
In UIKit you call UIFont.preferredFont(forTextStyle:) to get a semantically scaled font, set adjustsFontForContentSizeCategory = true on labels, and observe UIContentSizeCategoryDidChangeNotification to relayout when the user changes the setting. SwiftUI handles all three steps automatically when you use built-in Font styles, making it considerably less boilerplate.
Last reviewed: 2026-05-11 by the Soarias team.