Docs
Text

Expandable Text

A paragraph clamped to a line limit that shows a glass more pill only when it is cut, and grows to full height without reflow.

$npx swiftpieces add ExpandableText
See it in a Pro app On a real screen in one of the Pro apps: open it with Pro, take it apart, remix it, and copy the SwiftUI.
Free · MIT + Commons ClauseiOS 17.0+Liquid Glasstextread-moretruncationexpandcollapseline-limitreview
Type
ExpandableText
Files
ExpandableText.swift
Depends on
Nothing (Apple frameworks only)
Version
1.2.0

A "Read more" paragraph that shows a more pill only when the text is actually truncated. SwiftUI cannot tell you whether a Text was truncated, so this measures three hidden copies at the real width: the full text, the text at lineLimit lines, and the text at one line fewer (which locates the last visible line). The copies re-measure on width, Dynamic Type and text changes. When the full height exceeds the limited height, the visible text is laid out in full and clipped to the limited height; the end of the last line fades out (a transparent mask, so it works over any background) and a small glass pill reading more sits in the faded space on the trailing edge. Expanding animates the clip to the full height, so lines never reflow or jump, and the pill rides the text's bottom edge down to sit under it, its label morphing to less. The toggle runs in one transaction, so a card or row around the piece grows and shrinks with the text. Font and color come from the environment like Text: apply .font(...), .foregroundStyle(...) and .multilineTextAlignment(...) to the piece. VoiceOver reads the full text in both states and gets "Show more" / "Show less" actions; the visual pill is hidden from it.

Notes

  • SwiftUI has no API that reports truncation. The piece lays out three hidden copies at the paragraph's real width: the full text, the text at lineLimit lines, and the text at one line fewer. The "more" pill appears only when the full height is taller than the clamped height, so a short text never shows it.
  • The copies re-measure whenever the width, Dynamic Type size, font or text changes (onGeometryChange on iOS 18, a GeometryReader on iOS 17). They never read the expanded state, so there is no layout loop.
  • Until the first measurement lands, a plain lineLimit Text shows the same first lines, so first appearance does not jump.
  • Collapsed, the full text is clipped to the clamped height. The end of the last line fades to transparent through a mask and the link sits in the faded space as a small liquid glass pill reading "more", centred on the line. The mask needs no background color, so it works over cards, images and materials. Set Style.fade to paint a gradient into a solid color instead.
  • Expanding animates the clip height to the full height. Lines are laid out once at full length, so nothing reflows or jumps. The text unfurls on a calm spring with no visible overshoot and tucks back a little quicker. The pill rides the text's bottom edge on the same curve: down from the last line to hang just under the text as it opens, its label morphing letter by letter from "more" to "less", and back up as it closes. The fade at the end of the last line fills back in as the pill leaves it and returns a beat into the collapse, once the fold has passed most of the lines below it. This holds whether the piece is pinned at the top, the bottom or the center. With showsLess: false the paragraph stays open, and the pill sinks away once it does.
  • The pill is neutral glass, so it stays quiet inside reading text; its label is Style.link. On iOS 26 it is Liquid Glass and swells under a press; before iOS 26 it is frosted; under Reduce Transparency it is an opaque fill. The labels are localized keys, so the morph is Text's own letter-by-letter content transition.
  • Taps on the pill, the body and the VoiceOver actions toggle inside one animation, so a card or row around the piece grows and shrinks with the text. If you drive isExpanded yourself, change it inside withAnimation and the text folds on your animation together with its container; changed without one, the paragraph still folds on its own curve but its container jumps to the new height. The faded line end keeps its own timing either way, tuned to the piece's collapse.
  • Font, color and alignment come from the environment, like Text: .font, .foregroundStyle, .multilineTextAlignment.
  • AttributedString keeps bold, italics and links. With togglesOnTap, tapping the body toggles too; a tap that opens a link does not toggle, and lines hidden below the fold have no hit area. Links open through your openURL environment action.
  • isExpanded lets you drive or observe the state, for example to keep one review open at a time.
  • RTL: the fade and the pill sit on the trailing side, which is the left in right-to-left languages.
  • Style sets the pill's label color and weight (semibold, the house weight, by default), fade width (scaled with Dynamic Type) and the optional solid fade color. .disabled(true) dims the pill's label and stops toggling. All of the piece's type is semibold.
  • Reduce Motion: the text opens in a 0.2 second ease and closes in 0.18 seconds, with no spring. The pill still moves with the bottom edge of the text and its label cross-fades; the fade waits until the collapse is landing.
  • VoiceOver reads the full text in both states, with links in the rotor. "Show more" and "Show less" actions resize the paragraph for sighted VoiceOver users; the visual pill is hidden from VoiceOver.
  • The pill draws at one line tall but has a hit area of at least 44 by 44 points. There are no haptics: this is reading content.

Usage

ExpandableTextExample()

Parameters

ParameterDescription
textThe paragraph. A String is shown verbatim; an AttributedString keeps bold, italics and tappable links.
lineLimitLines shown while collapsed. Values below 1 are treated as 1.
isExpandedOptional binding to drive or observe the state from outside. nil keeps the state internally.
moreLabelThe pill's label on the last collapsed line. Localized from your app's strings; defaults to "more".
lessLabelThe pill's label under the expanded text. Localized from your app's strings; defaults to "less".
showsLessWhen false, an expanded paragraph stays expanded (the pill leaves once it opens, and tapping the body does nothing).
togglesOnTapWhen true, tapping anywhere on the paragraph also expands or collapses it. Links inside the text still open.
stylePill label color and weight, fade width, and an optional solid fade color. Defaults to the house palette.

Install

Pick one. Run it from the folder that contains your .xcodeproj and the files land inside your app.

Terminal

$npx swiftpieces add ExpandableText

Or ask your coding agent · Claude Code, Cursor or Xcode, once the MCP server is connected

>Add the SwiftPieces "Expandable Text" piece to my app

Or copy the source above into your app.

New to SwiftUI?If you know React, CSS or Tailwind, SwiftUI is easier to pick up than it looks.Try real app patterns first

Building a whole app? See SwiftPieces Pro →

All text →

Build it yourself: SwiftUI animations guide · Liquid Glass guide

On this page

SwiftPiecesPro

Build the whole app with one library.

Production-ready screens, whole flows, complete app templates, a Build Kit your agent follows and Pro remixing.

54 screens · 46 patterns · 48 flows · 12 templates · 31 Build Kit skills

Explore Pro