Skip to content
Highlight Sweep preview
An animated render of the SwiftUI source on this page.

Highlight Sweep

From syxUI — written for both platforms, not translated between them.

A marker-pen highlight that grows behind a phrase as you read it.

Text Effects · SwiftUI · Flutter · iOS 17 · Flutter 3.27

  • text
  • highlight
  • marker
  • sweep
  • emphasis
  • editorial
  • animation

The actual source

HighlightSweep.swift
// Highlight Sweep · syxUI · https://syxui.dev/components/highlight-sweep
// Free in any project. Keep this line and credit syxUI where a person can read it.

import SwiftUI

/// A marker-pen highlight that grows behind a phrase as you read it.
///
/// One `Text`, one band — no per-character work at all. The band composites
/// with `.multiply` so it tints whatever is behind the whole view like ink on
/// paper rather than sitting on top as a flat opaque block; `.compositingGroup()`
/// is what keeps that blend contained to this view instead of leaking into
/// unrelated content elsewhere on screen.
struct HighlightSweep: View {
    var text: String = "worth remembering"
    var highlight: Color = Color(red: 1.0, green: 0.878, blue: 0.4)
    /// One of "block", "underline", "thin", "swash".
    var style: String = "block"
    var duration: Double = 520
    /// Fraction of the line height the band covers.
    var height: Double = 0.72
    var radius: Double = 3
    /// Preview override. When set, renders that point of the one-shot sweep.
    var progress: Double? = nil

    @Environment(\.accessibilityReduceMotion) private var reduceMotion
    @State private var animated: Double = 0

    private let fontSize: CGFloat = 30

    var body: some View {
        let a = reduceMotion ? 1 : (progress ?? animated)

        return Text(text)
            .font(.system(size: fontSize, weight: .semibold))
            .background(alignment: .leading) {
                GeometryReader { g in
                    band(size: g.size, a: a)
                }
                .accessibilityHidden(true)
            }
            .compositingGroup()
            .onAppear(perform: start)
    }

    private func band(size: CGSize, a: Double) -> some View {
        // Underline/thin ignore `height` and sit as a fixed-thickness bar on
        // the baseline; block/swash use `height` as a fraction of the line,
        // offset up from the baseline plus a small nudge so it clears
        // descenders without reaching the cap line.
        let (bandHeight, yOffset): (CGFloat, CGFloat) = {
            switch style {
            case "underline": return (6, size.height - 6)
            case "thin": return (2, size.height - 2)
            default: return (size.height * height, size.height * (1 - height) - fontSize * 0.1)
            }
        }()
        // `swash` drifts a little vertically across the sweep — a raised
        // sine peaking mid-flight — so the band reads as hand-drawn.
        let drift: CGFloat = style == "swash" ? CGFloat(sin(a * .pi)) * 2 : 0
        let rotation: Double = style == "swash" ? 1.5 : 0

        return RoundedRectangle(cornerRadius: radius, style: .continuous)
            .fill(highlight)
            .frame(width: size.width * a, height: bandHeight)
            .offset(y: yOffset + drift)
            .rotationEffect(.degrees(rotation), anchor: .leading)
            .blendMode(.multiply)
    }

    private func start() {
        guard progress == nil, !reduceMotion else { return }
        withAnimation(.easeOut(duration: duration / 1000)) {
            animated = 1
        }
    }
}

#Preview {
    HighlightSweep()
        .frame(width: 380, height: 240)
        .padding(24)
}
iOS 17 · No dependencies

SwiftUI note. `.compositingGroup()` is required or `.multiply` blends against whatever sits behind the whole page instead of staying local to this view.

Dependencies

SwiftUI
No external dependencies

Requires iOS 17

Flutter
No external dependencies

Requires Flutter 3.27