```html SwiftUI: How to Build Dynamic Type (iOS 17+, 2026)

How to Build Dynamic Type in SwiftUI

iOS 17+ Xcode 16+ Beginner APIs: Font, dynamicTypeSize Updated: May 11, 2026
TL;DR

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

  1. 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.
  2. @Environment(\.dynamicTypeSize) exposes the current size category. The isAccessibilitySize computed property compares it against .accessibility1 (the first of five extra-large categories) and switches the row from HStack to VStack so text never wraps awkwardly beside the icon.
  3. .dynamicTypeSize(...DynamicTypeSize.xxxLarge) clamps the icon. The one-sided range operator pins the icon's own scaling ceiling at xxxLarge. The icon stays recognisably small even at accessibility5, while surrounding text can still grow unconstrained.
  4. 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 a VStack, giving very large text the full row width to breathe.
  5. 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

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.

```