Skip to content

Spec: Text

suragch edited this page Dec 28, 2023 · 50 revisions

"A view that displays one or more lines of read-only text."

https://developer.apple.com/documentation/swiftui/text/

Typography [TODO: Jonathan]

The goal of the Flutter Text widget is to be a near pixel-perfect replica of SwiftUI's Text view. For typography that means that the font, size, text decorations, and alignments should be visually indistinguishable from what Swift UI gives.

To familiarize yourself with typography in Apple, view the following resources:

As you implement the API, take into account the following topics:

Default system fonts

If no font is specified, the Text widget should use the default system font. On iOS this font is determined by the environment. Currently the default is San Francisco but this comes in different families:

  • SF Pro
  • SF Pro Rounded
  • SF Compact
  • SF Compact Rounded
  • SF Mono

The family that is used is different on different device types. For example, the Pro fonts are used on iOS, iPadOS, macOS, and tvOS. However, watchOS uses the Compact fonts by default. The Flutter Text widget must display the correct font family for every device and platform when no custom font is specified by the developer.

Custom fonts

Apple has many different system fonts that come built into to the OS. These vary depending on the platform and version. The complete list is available on the System Fonts page.

To select one of these fonts, use the Font.custom parameter on font:

Text("Arial").font(.custom("Arial", size: 30))
Text("Chalkduster").font(.custom("Chalkduster", size: 30))
Text("Party LET").font(.custom("Party LET", size: 30))
Text("Snell Roundhand").font(.custom("Snell Roundhand", size: 30))
Text("Zapfino").font(.custom("Zapfino", size: 30))
Screenshot 2023-12-27 at 17 13 11

You can also include one of your own fonts in the app bundle. You would use it by referring to the name the same way as shown above.

Font size

Font size is measured in points where points are a pixel density independent value. That is, a specific point size should have roughly the same physical size regardless of the device's screen pixel density.

In SwiftUI, you set the font size like so:

Text("size 6").font(.system(size: 6))
Text("size 7").font(.system(size: 7))
Text("size 8").font(.system(size: 8))
Text("size 9").font(.system(size: 9))
Text("size 10").font(.system(size: 10))
Text("size 11").font(.system(size: 11))
Text("size 12").font(.system(size: 12))
Text("size 13").font(.system(size: 13))
Text("size 14").font(.system(size: 14))
Text("size 15").font(.system(size: 15))
Text("size 16").font(.system(size: 16))
Text("size 17").font(.system(size: 17))
Text("size 18").font(.system(size: 18))
Text("size 19").font(.system(size: 19))
Text("size 20").font(.system(size: 20))
Text("size 21").font(.system(size: 21))
Text("size 22").font(.system(size: 22))
Text("size 23").font(.system(size: 23))
Text("size 24").font(.system(size: 24))
Text("size 25").font(.system(size: 25))
Text("size 26").font(.system(size: 26))
Text("size 27").font(.system(size: 27))
Text("size 28").font(.system(size: 28))
Text("size 29").font(.system(size: 29))
Text("size 30").font(.system(size: 30))
Text("size 31").font(.system(size: 31))
Text("size 32").font(.system(size: 32))
Text("size 33").font(.system(size: 33))
Text("size 34").font(.system(size: 34))
Text("size 35").font(.system(size: 35))
Text("size 36").font(.system(size: 36))
Text("size 37").font(.system(size: 37))
Text("size 38").font(.system(size: 38))
Text("size 39").font(.system(size: 39))
Text("size 40").font(.system(size: 40))
Text("size 41").font(.system(size: 41))
Text("size 42").font(.system(size: 42))
Text("size 43").font(.system(size: 43))
Text("size 44").font(.system(size: 44))
Text("size 45").font(.system(size: 45))
Text("size 46").font(.system(size: 46))
Text("size 47").font(.system(size: 47))
Text("size 48").font(.system(size: 48))
Text("size 49").font(.system(size: 49))
Text("size 50").font(.system(size: 50))
Text("size 51").font(.system(size: 51))
Text("size 52").font(.system(size: 52))
Text("size 53").font(.system(size: 53))
Text("size 54").font(.system(size: 54))
Text("size 56").font(.system(size: 56))
Text("size 58").font(.system(size: 58))
Text("size 60").font(.system(size: 60))
Text("size 62").font(.system(size: 62))
Text("size 64").font(.system(size: 64))
Text("size 66").font(.system(size: 66))
Text("size 68").font(.system(size: 68))
Text("size 70").font(.system(size: 70))
Text("size 72").font(.system(size: 72))
Text("size 76").font(.system(size: 76))
Text("size 80").font(.system(size: 80))
Text("size 84").font(.system(size: 84))
Text("size 88").font(.system(size: 88))
Text("size 92").font(.system(size: 92))
Text("size 96").font(.system(size: 96))

Flutter also has the notion of pixel density independent font sizes. However, it shouldn't be assumed that SwiftUI point sizes and Flutter fonts size values are the same. If any conversion needs to happen, this SwiftUI port should ensure that the size values produce the same sized font on the screen.

font sizes

Font weight

Fonts have the following weights defined in order of increasing thickness:

  • ultraLight
  • thin
  • light
  • regular
  • medium
  • semibold
  • bold
  • heavy
  • black
font weights

They are implemented in SwiftUI using fontWeight like so:

Text("ultraLight").fontWeight(.ultraLight)
Text("thin").fontWeight(.thin)
Text("light").fontWeight(.light)
Text("regular").fontWeight(.regular)
Text("medium").fontWeight(.medium)
Text("semibold").fontWeight(.semibold)
Text("bold").fontWeight(.bold)
Text("heavy").fontWeight(.heavy)
Text("black").fontWeight(.black)

Or by selecting the weight parameter on the font:

Text("ultraLight").font(.system(size: 30, weight: .ultraLight))
Text("thin").font(.system(size: 30, weight: .thin))
Text("light").font(.system(size: 30, weight: .light))
Text("regular").font(.system(size: 30, weight: .regular))
Text("medium").font(.system(size: 30, weight: .medium))
Text("semibold").font(.system(size: 30, weight: .semibold))
Text("bold").font(.system(size: 30, weight: .bold))
Text("heavy").font(.system(size: 30, weight: .heavy))
Text("black").font(.system(size: 30, weight: .black))

Font design

There are four choices for font design:

  • default: SF Pro font.
  • rounded: SF Pro Rounded font.
  • serif: New York font.
  • monospaced: SF mono font.
font designs

They are implemented in SwiftUI using fontDesign:

Text("default").fontDesign(.default)
Text("rounded").fontDesign(.rounded)
Text("serif").fontDesign(.serif)
Text("monospaced").fontDesign(.monospaced)

Or, alternatively, using the design parameter of the font:

Text("default").font(.system(size: 30, design: .default))
Text("rounded").font(.system(size: 30, design: .rounded))
Text("serif").font(.system(size: 30, design: .serif))
Text("monospaced").font(.system(size: 30, design: .monospaced))

Font width

Fonts have the following widths defined in order of increasing thickness. The height for all of these is the same.

  • compressed
  • condensed
  • standard
  • expanded
Font widths

They are implemented in SwiftUI like so:

Text("compressed").fontWidth(.compressed)
Text("condensed").fontWidth(.condensed)
Text("standard").fontWidth(.standard)
Text("expanded").fontWidth(.expanded)

Text scale

Text.scale does not allow you to set an arbitrary scale but rather two logical scales:

  • default
  • secondary
Text.scale

They can be applied like so:

Text("default").textScale(.default)
Text("secondary").textScale(.secondary)

Text style

The following styles are available to text:

  • bold
  • italic
  • monospaced: All characters have the same width.
  • monospacedDigit: Digits have the same width while other characters use the default font.
  • textcase: Text is rendered as all uppercase or lowercase regardless of the case of the underlying string.
text styles

These are implemented in SwiftUI like so:

Text("bold").bold()
Text("italic").italic()
Text("monospaced").monospaced()
Text("monospacedDigit 123").monospacedDigit()
Text("textCase uppercase").textCase(.uppercase)
Text("textCase lowercase").textCase(.lowercase)

Text decoration

There are two different decorations that you can add to text:

  • underlined: Users can select the color and pattern of the underline decoration.
  • strikethrough: Users can select the color and pattern of the strikethrough decoration.

Underline is implemented like this:

Text("underline").underline()
Text("red").underline(color: .red)
Text("solid").underline(pattern: .solid)
Text("dot").underline(pattern: .dot)
Text("dash").underline(pattern: .dash)
Text("dashDot").underline(pattern: .dashDot)
Text("dashDotDot").underline(pattern: .dashDotDot)
underline decoration

And strikethrough like this:

Text("strikethrough").strikethrough()
Text("red").strikethrough(color: .red)
Text("solid").strikethrough(pattern: .solid)
Text("dot").strikethrough(pattern: .dot)
Text("dash").strikethrough(pattern: .dash)
Text("dashDot").strikethrough(pattern: .dashDot)
Text("dashDotDot").strikethrough(pattern: .dashDotDot)
strikethrough decoration

Note: Shadow is not currently implemented in SwiftUI, but will likely be implemented in the future. We could preemptively implement it in Flutter.

Foreground style

You can set the text foreground style using foregroundStyle. This takes a ShapeStyle, which can be a system color:

foreground colors
Text("black").foregroundStyle(.black)
Text("blue").foregroundStyle(.blue)
Text("brown").foregroundStyle(.brown)
Text("cyan").foregroundStyle(.cyan)
Text("gray").foregroundStyle(.gray)
Text("green").foregroundStyle(.green)
Text("indigo").foregroundStyle(.indigo)
Text("mint").foregroundStyle(.mint)
Text("orange").foregroundStyle(.orange)
Text("pink").foregroundStyle(.pink)
Text("purple").foregroundStyle(.purple)
Text("red").foregroundStyle(.red)
Text("teal").foregroundStyle(.teal)
Text("white").foregroundStyle(.white)
Text("clear").foregroundStyle(.clear)

Or a gradient:

foreground gradients
Text("linearGradient")
    .foregroundStyle(.linearGradient(
        colors: [.red, .blue, .green, .yellow],
        startPoint: .leading,
        endPoint: .trailing
    ))
Text("AngularGradient")
    .foregroundStyle(AngularGradient(
        colors: [.red, .blue, .green, .yellow],
        center: .center
    ))
Text("conicGradient")
    .foregroundStyle(.conicGradient(
        colors: [.red, .blue, .green, .yellow],
        center: .center
    ))
Text("ellipticalGradient")
    .foregroundStyle(.ellipticalGradient(
        colors: [.red, .blue, .green, .yellow]
    ))
Text("RadialGradient")
    .foregroundStyle(RadialGradient(
        colors: [.red, .blue, .green, .yellow],
        center: .center,
        startRadius: 30,
        endRadius: 100
    ))

Or a material:

Screenshot 2023-12-28 at 18 34 09
VStack(alignment: .leading) {
    Text("ultraThinMaterial").foregroundStyle(.ultraThinMaterial)
    Text("thinMaterial").foregroundStyle(.thinMaterial)
    Text("regularMaterial").foregroundStyle(.regularMaterial)
    Text("thickMaterial").foregroundStyle(.thickMaterial)
    Text("ultraThickMaterial").foregroundStyle(.ultraThickMaterial)
}.background(.blue)

You can also apply other effects like filling the text with an image. This section does not cover all of the possibilities for ShapeStyle.

Background

The background modifier also takes a ShapeStyle:

background
Text("background color").background(.yellow)
Text("background gradient").background(.linearGradient(
    colors: [.red, .blue],
    startPoint: .leading,
    endPoint: .trailing
))

There are many more options for background, but this goes beyond the scope of typography.

Predefined text styles

SwiftUI predefines a list of text styles for various purposes within an app:

  • largeTitle
  • title
  • title2
  • title3
  • headline
  • body
  • callout
  • subheadline
  • footnote
  • caption1
  • caption2
predefined text styles

They can be applied to a Text view like so:

Text("largeTitle").font(.largeTitle)
Text("title").font(.title)
Text("title2").font(.title2)
Text("title3").font(.title3)
Text("headline").font(.headline)
Text("body").font(.body)
Text("callout").font(.callout)
Text("subheadline").font(.subheadline)
Text("footnote").font(.footnote)
Text("caption").font(.caption)
Text("caption2").font(.caption2)

Each of those styles are built from specific values for the following three components:

  • Weight
  • Size (points)
  • Leading (points)

The values for weight, size, and leading vary depending on the following contexts:

  • Dynamic Type (xSmall, Small, Medium, Large, xLarge, xxLarge, xxxLarge)
  • Accessibility (AX) level (AX1, AX2, AX3, AX4, AX5)
  • Platform (iOS, ipadOS, macOS, tvOS, watchOS)

The specific numerical values are provided in the SwiftUI typography specs. Flutter needs to match all of these.

Dynamic Type

Dynamic Type refers to text that is scaled within the based on user choice in the device's (not the app's) system settings. Dynamic type is not available on macOS, but on iOS it is available by going to Settings > Display & Brightness > Text Size. This gives you access to seven sizes, which in the documentation are known as:

  • xSmall
  • Small
  • Medium
  • Large
  • xLarge
  • xxLarge
  • xxxLarge

You can gain access to five additional sizes by going to Settings > Accessibility > Display & Text Size > Larger Text > Larger Accessibility Sizes. These sizes are referred to in the documentation as:

  • AX1
  • AX2
  • AX3
  • AX4
  • AX5

Dynamic Type affects all of the predefined text styles, but as an example, here is the font point size for the body style on iOS and ipadOS:

  • xSmall: 14
  • Small: 15
  • Medium: 16
  • Large: 17
  • xLarge: 19
  • xxLarge: 21
  • xxxLarge: 23
  • AX1: 28
  • AX2: 33
  • AX3: 40
  • AX4: 47
  • AX5: 53

Here are images of how the twelve Dynamic Type settings affect each of the predefined text styles:

xSmall Small Medium Large xLarge xxLarge xxxLarge AX1 AX2 AX3 AX4 AX5

All of those used the same code:

Text("largeTitle").font(.largeTitle)
Text("title").font(.title)
Text("title2").font(.title2)
Text("title3").font(.title3)
Text("headline").font(.headline)
Text("body").font(.body)
Text("callout").font(.callout)
Text("subheadline").font(.subheadline)
Text("footnote").font(.footnote)
Text("caption").font(.caption)
Text("caption2").font(.caption2)

Only the Dynamic Type setting was adjusted.

Note: Rather than running the simulator and changing the settings to see the effect of each of these, you can view them easily in the Xcode Preview window by clicking the Device Settings button:

Device Settings: Dynamic Type

Dynamic Type is applied to the system font only if you specify one of the predefined text styles.

// Dynamic Type not applied
Text("body")

// Dynamic Type applied
Text("body").font(.body)

// Dynamic Type not applied
Text("body").font(.system(size: 10))
Dynamic Type with system font

With a custom font, Dynamic Type is applied if you use the size parameter but not applied if you use the fixedSize parameter.

// Dynamic Type applied
Text("body").font(.custom("arial", size: 30))

// Dynamic Type overridden
Text("body").font(.custom("arial", fixedSize: 30))
Dynamic Type with custom font

When applied, the scaling is done in relation to the body size by default, but you can select another style to scale with by using the relativeTo parameter.

// Dynamic Type applied (relative to body)
Text("body").font(.custom("arial", size: 30, relativeTo: .body))

// Dynamic Type applied (relative to largeTitle)
Text("body").font(.custom("arial", size: 30, relativeTo: .largeTitle))
Dynamic Type relative to style

You can also adjust other values such as padding in response to the user's current Dynamic Type setting. To achieve this, use the @ScaledMetric property wrapper:

struct MyView: View {
    @ScaledMetric var scale = 100.0

    var body: some View {
        Text("ScaledMetric: \(scale)")
            .padding(.leading, scale / 10)

Here are the values of how 100.0 is scaled for each of the Dynamic Type settings:

  • xSmall: 86.33
  • Small: 91.0
  • Medium: 95.33
  • Large: 100.0
  • xLarge: 109.0
  • xxLarge: 118.33
  • xxxLarge: 131.67
  • AX1: 154.67
  • AX2: 181.67
  • AX3: 218.33
  • AX4: 254.67
  • AX5: 281.67

The scale depends on the initial value and is not precisely the same scale for other initial values. For example, if the initial value is 1.0, the scale will never go below 1.0, even for xSmall.

TODO: If the ScaledMetric value is not available from the system, we will need to do more work to discover the scaling algorithm.

Truncation mode

Truncation mode affects where ellipses are added to a string to indicate truncated text when the entire string doesn't fit in the allotted space. There are three options for truncationMode:

  • tail: Cut off the end of the string and add the ellipses there.
  • middle: Cut off the middle of the string and add the ellipses there.
  • head: Cut off the beginning of the string and add the ellipses there.
truncation mode LTR
Text("Brevity is the soul of wit.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .truncationMode(.tail)

Text("Brevity is the soul of wit.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .truncationMode(.middle)

Text("Brevity is the soul of wit.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .truncationMode(.head)

Be aware that head is on the right and tail on the left for right-to-left (RTL) languages like Arabic or Hebrew.

truncation mode RTL
Text("هذا سطر طويل من النص")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .truncationMode(.tail)

Text("هذا سطر طويل من النص")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .truncationMode(.middle)

Text("هذا سطر طويل من النص")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .truncationMode(.head)

Note: That was a Google translate of "This is a long line of text." into Arabic, so it might not be grammatically correct.

Allow tightening

You can squeeze in a little more text if you set the allowsTightening flag to true. This will reduce the space around characters in a string by just a little. See also the sections on kerning and tracking. In the following image, the first line allows tightening while the second doesn't:

tightening
Text("Brevity is the soul of wit.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .allowsTightening(/*@START_MENU_TOKEN@*/true/*@END_MENU_TOKEN@*/)

Text("Brevity is the soul of wit.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .allowsTightening(false)

Scaling text down to fit

Another option to make text fit in a small space is to scale it down. For this you can set the minimumScaleFactor to a value between 0.0 and 1.0. The default is 1.0. SwiftUI then attempts to make the entire string fit by scaling down the size. But only up to a point. If the string still doesn't fit, it will be truncated.

minimumScaleFactor
Text("Brevity is the soul of wit.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .minimumScaleFactor(1.0)

Text("Brevity is the soul of wit.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .minimumScaleFactor(0.7)

Text("Brevity is the soul of wit.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .minimumScaleFactor(0.5)

Text("Brevity is the soul of wit.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .minimumScaleFactor(0.3)

Text("Brevity is the soul of wit.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
    .minimumScaleFactor(0.1)

Baseline offset

The baseline is the line on which text normally sits, though some letters may go below the baseline. In the image below, you can see the "y" goes below the baseline:

baseline

In SwiftUI you can move text above or below the baseline using baselineOffset:

HStack() {
    Text("lower").baselineOffset(-10)
    Text("normal")
    Text("higher").baselineOffset(10)
}
baselineOffset

Kerning and tracking

Kerning refers to the spacing between glyphs. Some letters look better when closer together and some look better when further apart. Because of this, spacing is built into the font itself. You can see this in the example below. The "V" and "A" partially overlap when placed next to each other.

VA kerning

Even though the font normally handles kerning, you can also adjust the character offsets in SwiftUI by setting the kerning modifier like so:

Text("kerning").kerning(-3)
Text("kerning").kerning(-2)
Text("kerning").kerning(-1)
Text("kerning").kerning(0)
Text("kerning").kerning(1)
Text("kerning").kerning(2)
Text("kerning").kerning(3)

Here is the result:

kerning

You can accomplish something similar with the tracking modifier. Tracking adds or removes white space after every character. Take the example from above but replace kerning with tracking:

Text("tracking").tracking(-3)
Text("tracking").tracking(-2)
Text("tracking").tracking(-1)
Text("tracking").tracking(0)
Text("tracking").tracking(1)
Text("tracking").tracking(2)
Text("tracking").tracking(3)
tracking

With the text and font used in the examples above, there is no difference between kerning and tracking. Where kerning makes a difference is when the font contains ligatures. Ligatures are special glyphs used to display a combination of characters. For example, some fonts have a ligature for "fl" and "fi". Apparently the San Francisco fonts do not, but it is visible in the system Zapfino font.

Text("flight")
    .font(.custom("Zapfino", size: 20))

Text("flight")
    .font(.custom("Zapfino", size: 20))
    .kerning(20)

Text("flight")
    .font(.custom("Zapfino", size: 20))
    .tracking(20)

The first example is the word "flight" without kerning or tracking. The second example uses kerning to separate the characters while still maintaining the "fl" ligature. The third example uses tracking to add space after every character and ignores the ligature.

ligature maintained by kerening

So, use kerning when you care about maintaining ligatures at the character level. Otherwise, use tracking.

Note that SwiftUI uses preset tracking values for every font point size of the default fonts for iOS, iPadOS, and visionOS, macOS, and tvOS. Flutter needs to match these.

Multiline text alignment

Multiline text can have the following alignments in SwiftUI:

  • leading: The text is left justified.
  • center: The text is centered on every line.
  • trailing: The text is right justified.
Multiline text alignment
Text("To be, or not to be, that is the question:")
    .frame(width: 120)
    .border(.black)
    .multilineTextAlignment(.leading)
Text("To be, or not to be, that is the question:")
    .frame(width: 120)
    .border(.black)
    .multilineTextAlignment(.center)
Text("To be, or not to be, that is the question:")
    .frame(width: 120)
    .border(.black)
    .multilineTextAlignment(.trailing)

One would expect that in RTL contexts, leading and trailing would have the opposite justifications, but that is apparently not the case:

RTL multiline text alignment
Text("هذا سطر طويل من النص")
    .frame(width: 150)
    .border(.black)
    .multilineTextAlignment(.leading)
Text("هذا سطر طويل من النص")
    .frame(width: 150)
    .border(.black)
    .multilineTextAlignment(.center)
Text("هذا سطر طويل من النص")
    .frame(width: 150)
    .border(.black)
    .multilineTextAlignment(.trailing)

Currently justified text is not supported in SwiftUI. We could proactively support it in Flutter.

Line limit

You can limit the number of lines that are shown in multiline text by using lineLimit. A default of nil will show all of the lines.

line limit
Text("To be, or not to be, that is the question.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(1)
Text("To be, or not to be, that is the question.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(2)
Text("To be, or not to be, that is the question.")
    .frame(width: 150)
    .border(.black)
    .lineLimit(nil)

Line spacing

You can use lineSpacing to increase the amount of space between lines. In the first example below, there is no additional spacing. The second example adds 10 points of additional spacing.

lineSpacing
Text("To be, or not to be, that is the question.")
    .frame(width: 150)
    .border(.black)
    .lineSpacing(0.0)
Text("To be, or not to be, that is the question.")
    .frame(width: 150)
    .border(.black)
    .lineSpacing(10.0)

Localization

TODO (Matteo):

Accessibility

TODO (Vince):

Technical Design

TODO (Matt):

Clone this wiki locally