
Blur Text
From syxUI — written for both platforms, not translated between them.
A headline that resolves word by word out of a blur.
Text Effects · SwiftUI · Flutter · iOS 17 · Flutter 3.16
- text
- blur
- reveal
- headline
- animation
- typography
The actual source
BlurText.swift
// Blur Text · syxUI · https://syxui.dev/components/blur-text
// Free in any project. Keep this line and credit syxUI where a person can read it.
import SwiftUI
/// Text that resolves word by word out of a blur.
///
/// Leave `progress` nil and the reveal runs once on appear. Pass a value and
/// you drive it yourself — useful for scroll-linked reveals, or for holding a
/// specific frame in a screenshot.
struct BlurText: View {
var text: String = "Beautiful native components"
var font: Font = .system(size: 34, weight: .bold)
var progress: Double? = nil
var duration: Double = 1.2
/// How much of the timeline two neighbouring words share. Higher overlaps
/// more words at once and reads as softer.
var overlap: Double = 2.2
@Environment(\.accessibilityReduceMotion) private var reduceMotion
@State private var animated: Double = 0
private var words: [String] { text.split(separator: " ").map(String.init) }
private var current: Double { progress ?? animated }
var body: some View {
FlowLayout(spacing: 9, lineSpacing: 6) {
ForEach(Array(words.enumerated()), id: \.offset) { index, word in
let reveal = revealAmount(for: index)
Text(word)
.font(font)
.blur(radius: (1 - reveal) * 9)
.opacity(reveal)
.offset(y: (1 - reveal) * 12)
}
}
.onAppear {
guard progress == nil else { return }
guard !reduceMotion else { animated = 1; return }
withAnimation(.easeOut(duration: duration)) { animated = 1 }
}
// One label for the whole phrase — VoiceOver should not read it
// word by word.
.accessibilityElement(children: .ignore)
.accessibilityLabel(text)
}
private func revealAmount(for index: Int) -> Double {
let total = Double(words.count) + overlap
let start = Double(index)
let amount = (current * total - start) / overlap
return min(max(amount, 0), 1)
}
}
/// Minimal wrapping layout so words can animate individually.
/// Copy it along with BlurText — it is the only piece the effect depends on.
struct FlowLayout: Layout {
var spacing: CGFloat = 8
var lineSpacing: CGFloat = 6
func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) -> CGSize {
let maxWidth = proposal.width ?? .infinity
var x: CGFloat = 0, y: CGFloat = 0, lineHeight: CGFloat = 0, widest: CGFloat = 0
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
if x > 0 && x + size.width > maxWidth {
y += lineHeight + lineSpacing
x = 0
lineHeight = 0
}
x += size.width + spacing
widest = max(widest, x - spacing)
lineHeight = max(lineHeight, size.height)
}
return CGSize(width: min(widest, maxWidth), height: y + lineHeight)
}
func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout ()) {
var x = bounds.minX, y = bounds.minY, lineHeight: CGFloat = 0
for subview in subviews {
let size = subview.sizeThatFits(.unspecified)
if x > bounds.minX && x + size.width > bounds.maxX {
y += lineHeight + lineSpacing
x = bounds.minX
lineHeight = 0
}
subview.place(at: CGPoint(x: x, y: y), proposal: ProposedViewSize(size))
x += size.width + spacing
lineHeight = max(lineHeight, size.height)
}
}
}
#Preview {
BlurText()
.frame(width: 320)
.padding(30)
}
iOS 17 · No dependencies
SwiftUI note. FlowLayout is included because words must be laid out individually to animate individually. Under Reduce Motion the text appears fully resolved.
Dependencies
- SwiftUI
- No external dependencies
- Flutter
- No external dependencies
Requires iOS 17
Requires Flutter 3.16


