Skip to content
Blur Text preview
An animated render of the SwiftUI source on this page.

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

Requires iOS 17

Flutter
No external dependencies

Requires Flutter 3.16