Skip to content
All articles

SwiftUI Line Height: The iOS 26 lineHeight Modifier Explained

How SwiftUI line height works in iOS 26: .lineHeight presets, multiple(factor:), exact and leading values, vs lineSpacing, on iPhone and iPhone Duo.

By AppleDuo EditorialPublished 6 min read

Also available in:DeutschEspañolFrançais日本語Türkçe

On this page
  1. How do I set line height in SwiftUI?
  2. What do .tight, .normal and .loose mean?
  3. How does .multiple(factor:) compare to CSS line-height?
  4. Why does a one-line label look too high with lineHeight?
  5. What's the difference between lineHeight, lineSpacing and leading?
  6. How do I make numbers roll with contentTransition(.numericText())?
  7. Does line height matter more on iPhone Duo?
  8. Key takeaways

Designers hand over specs like "17 pt, line height 1.5," and for a long time SwiftUI had no direct way to say that. You approximated it with .lineSpacing, which adds points between lines, or with a font's .leading, which offers three subtle steps. iOS 26 fixes this with a real SwiftUI line height modifier.

This guide covers the lineHeight(_:) modifier, every value it accepts, how it compares to the older APIs, and the one behavior that surprises people: where the extra space goes. It's based on Apple's documentation, Natalia Panferova's write-up on Nil Coalescing and posts by iOS developer @sarunw. The modifier is new in iOS 26.0, and details can change between releases.

How do I set line height in SwiftUI?

Apply .lineHeight(_:) to a Text, or to any view above it. Apple describes it as "a modifier for the default line height in the view hierarchy," so it works through the environment, like .font:

Text(article.body)
    .font(.body)
    .lineHeight(.loose)

The parameter is an optional AttributedString.LineHeight, a Foundation type that Apple defines as the distance between the baselines of two subsequent lines. The default is nil, and in that case, Apple says, SwiftUI "automatically chooses an appropriate line height setting for each context." Pass nil to return a subtree to that automatic behavior.

The modifier and the matching lineHeight environment value are available from iOS 26.0, iPadOS 26.0, macOS 26.0, Mac Catalyst 26.0, tvOS 26.0, visionOS 26.0 and watchOS 26.0. There is no back-deployment, so gate it with #available(iOS 26.0, *) if you support older versions.

What do .tight, .normal and .loose mean?

AttributedString.LineHeight has four static properties and three factory methods. Apple's documentation describes each one:

ValueApple's descriptionScales with Dynamic Type
.tightMultiple of the point size perceived as tightYes
.normalMultiple of the point size perceived as normalYes
.looseMultiple of the point size perceived as looseYes
.variableBased on the ascent and descent of the fonts in each lineYes
.multiple(factor:)Largest point size multiplied by factorYes
.leading(increase:)Largest point size plus increase pointsPartly: the font grows, the increase doesn't
.exact(points:)A fixed total in pointsNo

Apple doesn't publish the numbers behind the three presets. According to @sarunw, they're shortcuts for .multiple(factor:): .tight is 1.0, .normal is 1.2 and .loose is 1.5. If you need a specific value, write the factor yourself so your code doesn't depend on that mapping.

Nil Coalescing's comparison found .normal and .variable almost indistinguishable from the default. The difference is in the definition: .variable follows each line's font metrics, while the other values are constant regardless of "the effective ascent and descent of the fonts in use," in Apple's words.

How does .multiple(factor:) compare to CSS line-height?

If you come from the web, .multiple(factor:) is the one you want. As @sarunw puts it, .lineHeight(.multiple(factor: 1.5)) works like CSS line-height: 1.5: the line height is the font size times the factor.

Apple's definition says the same thing, with one detail: the factor multiplies the largest point size in the line, so a line that mixes a 17 pt body with a 22 pt inline phrase uses 22 pt.

Text(article.body)
    .font(.body)
    .lineHeight(.multiple(factor: 1.5))

Because .body is a text style, the point size changes with the person's Dynamic Type setting, and the line height changes with it. Nil Coalescing confirms that multiple(factor:) keeps spacing proportional across Dynamic Type sizes.

The other two methods trade that flexibility for control:

Text(caption)
    .lineHeight(.leading(increase: 6))

Text(ticketNumber)
    .lineHeight(.exact(points: 24))

.leading(increase:) is the largest point size plus a fixed number of points, so the gap stays the same as the font grows. .exact(points:) ignores the font entirely. Nil Coalescing warns that with an exact value, lines can overlap or be cut off at larger text sizes, so keep it for short, fixed-size text, if at all.

Why does a one-line label look too high with lineHeight?

This is the catch. A browser splits the extra space of a CSS line height in half, above and below the text; the CSS specification calls it half-leading. According to @sarunw, SwiftUI's .lineHeight puts all of the extra space below each line, "even when there is only one." A one-line label with .multiple(factor: 2) therefore "looks like it sits too high."

In a paragraph, that's harmless: the space ends up between lines, which is what you wanted. In a single-line label inside a button, a chip or a centered row, the text sits at the top of a taller frame, and centering the frame doesn't center the text.

A few ways to handle it:

  • Scope the modifier. Since .lineHeight flows through the environment, apply it to the paragraph Text, not the whole screen. If a parent sets it, reset a label with .lineHeight(nil).
  • Align on baselines. In a row, use HStack(alignment: .firstTextBaseline) or .lastTextBaseline so labels line up by their text, not by their frames, and extra space below a line doesn't shift them.
  • Keep factors modest for short text. A large factor mainly helps long-form reading. Headlines and labels rarely need more than the default.
HStack(alignment: .firstTextBaseline) {
    Text(item.title)
        .font(.headline)
    Spacer()
    Text(item.price, format: .currency(code: "USD"))
        .font(.subheadline)
}
.lineHeight(nil)

Here the row ignores any line height set higher up, and the two labels align on their first baseline. Test both with large Dynamic Type sizes, since that's where spacing problems show up first.

What's the difference between lineHeight, lineSpacing and leading?

SwiftUI now has three ways to change the space between lines, and they measure different things:

APISinceWhat it setsUnit
.lineHeight(_:)iOS 26Baseline to baselineFactor or points
.lineSpacing(_:)iOS 13Bottom of one line to top of the nextPoints
Font.leading(_:)iOS 14The font's own line spacing.standard, .loose, .tight

lineSpacing(_:) adds a fixed gap between lines. That's useful, but it doesn't map to a design spec that gives line height as a multiple, and the gap doesn't grow with Dynamic Type. Apple's example uses .lineSpacing(10).

Font.leading(_:) is applied to a font, as in .font(.title.leading(.loose)). Apple notes that "the availability of leading adjustments depends on the font," and for some fonts the modifier has no effect. Nil Coalescing describes its effect as very subtle.

For new code on iOS 26 and later, .lineHeight is the one that matches how designers specify text. Keep .lineSpacing for older deployment targets.

How do I make numbers roll with contentTransition(.numericText())?

A related text effect, and a frequent request for scores, counters and prices: @sarunw points out that .contentTransition(.numericText()) "makes numbers roll to their new value." Instead of fading, each changing digit slides to its new value.

struct ScoreView: View {
    @State private var score = 0

    var body: some View {
        VStack {
            Text(score, format: .number)
                .font(.largeTitle.monospacedDigit())
                .contentTransition(.numericText(value: Double(score)))
            Button("Add 10") {
                withAnimation { score += 10 }
            }
        }
    }
}

Two details make it work. First, Apple's tip on contentTransition(_:) says it only has an effect within an animation, so change the value inside withAnimation. Second, numericText(value:), from iOS 17, uses the difference between the old and new values to pick the direction. The older numericText(countsDown:), from iOS 16, takes the direction as a Boolean instead.

The .monospacedDigit() font keeps the number's width steady while digits change. For animated symbols next to your numbers, see our guide to SF Symbols animations and rendering modes.

Does line height matter more on iPhone Duo?

On iPhone Duo, the inner display reports a regular width, like an iPad, while the outer display behaves like other iPhones (see our guide to iPhone Duo size classes). A reading view that spans the inner display can end up with much longer lines than on the outer one.

Longer lines usually read better with a bit more line height, and a value that looks right on the outer display can feel cramped across the inner one. Because .lineHeight is an environment value, you can tie it to the size class:

@Environment(\.horizontalSizeClass) private var horizontalSizeClass

var body: some View {
    Text(article.body)
        .font(.body)
        .lineHeight(.multiple(factor: horizontalSizeClass == .regular ? 1.5 : 1.3))
        .frame(maxWidth: 680)
}

The factors here are examples, not Apple guidance. Capping the width with frame(maxWidth:) often helps more than any line height, because it keeps line length comfortable on the wider display.

Key takeaways

  • iOS 26 adds .lineHeight(_:), which takes an AttributedString.LineHeight and flows through the environment. nil means automatic.
  • .multiple(factor: 1.5) works like CSS line-height: 1.5 and scales with Dynamic Type. Per @sarunw, .tight, .normal and .loose are factors 1.0, 1.2 and 1.5.
  • .leading(increase:) adds fixed points; .exact(points:) doesn't scale and can clip at large sizes.
  • All extra space goes below each line, so one-line labels look high. Scope the modifier and align rows on text baselines.
  • .lineHeight measures baseline to baseline; .lineSpacing adds a gap between lines; Font.leading offers three subtle steps.
  • For rolling numbers, use .contentTransition(.numericText(value:)) inside withAnimation.

Next, see how iOS 27 handles drag to reorder and swipe actions in SwiftUI, or browse games built for the fold for apps that already adapt to iPhone Duo's two displays.

Frequently asked questions

How do I set line height in SwiftUI?

On iOS 26 and later, apply .lineHeight(_:) to a Text or any parent view, for example .lineHeight(.multiple(factor: 1.5)). The value flows down through the environment. Passing nil restores SwiftUI's automatic line height.

What values do .tight, .normal and .loose use?

Apple documents them as constant line heights based on a multiple of the point size perceived as tight, normal and loose. Developer @sarunw reports that they equal .multiple(factor:) with 1.0, 1.2 and 1.5.

What is the difference between lineHeight and lineSpacing in SwiftUI?

lineSpacing(_:) adds space between the bottom of one line and the top of the next, in points. lineHeight(_:) sets the distance from one baseline to the next, either as a multiple of the font size or as a point value.

Does SwiftUI lineHeight scale with Dynamic Type?

multiple(factor:) does, because it multiplies the point size. leading(increase:) grows with the font but adds a fixed number of points. exact(points:) is a fixed total, so it does not scale and can clip or overlap text at large sizes.

Why does my one-line label look too high with lineHeight?

As @sarunw notes, .lineHeight places all of the extra space below each line, even when there is only one line. Apply large factors only to multiline text, and align single lines by their text baseline instead of centering their frames.

Sources

  1. lineHeight(_:) · Apple Developer Documentation
  2. AttributedString.LineHeight · Apple Developer Documentation
  3. multiple(factor:) · Apple Developer Documentation
  4. exact(points:) · Apple Developer Documentation
  5. leading(increase:) · Apple Developer Documentation
  6. lineSpacing(_:) · Apple Developer Documentation
  7. Font.Leading · Apple Developer Documentation
  8. numericText(value:) · Apple Developer Documentation
  9. contentTransition(_:) · Apple Developer Documentation
  10. iOS 26 adds a .lineHeight modifier to SwiftUI · @sarunw on X
  11. .lineHeight presets .tight, .normal, .loose = .multiple(factor:) 1.0, 1.2, 1.5 · @sarunw on X
  12. .lineHeight(.multiple(factor: 1.5)) works like CSS line-height: 1.5 · @sarunw on X
  13. .lineHeight puts all extra space below each line · @sarunw on X
  14. .contentTransition(.numericText()) makes numbers roll to their new value · @sarunw on X
  15. Adjusting line height in SwiftUI on iOS 26 · Nil Coalescing
  16. CSS 2.2: Visual formatting model details (line-height) · W3C

Make the fold part of your app.

We showcase apps and games that do something real with iPhone Duo. Send us yours with a link to it on the App Store, or see what other developers have built.

[email protected]