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

Line Shadow Text

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

Extruded type whose shadow is a hatch of lines that flows as you watch.

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

  • text
  • shadow
  • hatch
  • extrude
  • poster
  • editorial
  • animation

The actual source

LineShadowText.swift
// Line Shadow Text · syxUI · https://syxui.dev/components/line-shadow-text
// Free in any project. Keep this line and credit syxUI where a person can read it.

import SwiftUI

/// Extruded type whose shadow is a hatch of lines that flows as you watch.
///
/// The back copy is a `Canvas` of parallel stripes clipped to the glyph
/// shapes via `.mask`, offset down-right by `depth`; the front copy is a
/// plain, solid `Text`. Canvas contents are one of the properties
/// `withAnimation` genuinely interpolates (`ripple-grid` is the existing
/// proof), so the flow needs no `TimelineView` — a `@State` fraction driven
/// linearly and read straight into the stripe geometry is enough.
struct LineShadowText: View {
    var text: String = "DEPTH"
    var shadowColor: Color = Color(red: 0.059, green: 0.384, blue: 0.996)
    /// Shadow offset, in em.
    var depth: Double = 0.06
    var stripe: Double = 4
    /// Flow speed, in pt/s.
    var speed: Double = 8
    var angle: Double = 45
    /// Preview override. One full cycle is exactly one stripe period.
    var phase: Double? = nil

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

    private let fontSize: CGFloat = 64

    private var current: Double { reduceMotion ? 0 : (phase ?? animated) }
    /// Seconds for the flow to advance by exactly one stripe period — the
    /// point at which the hatch looks identical to where it started.
    private var loopPeriod: Double { max(stripe * 2 / max(speed, 0.1), 0.1) }

    var body: some View {
        // Preview scrubbing wants the settled, fully-extruded look; only the
        // live, self-driven path grows `depth` in on appear.
        let depthPt = CGFloat(depth) * fontSize * (phase == nil ? grown : 1)
        let flow = CGFloat(current) * CGFloat(stripe * 2)

        ZStack {
            Canvas { context, size in
                hatch(in: &context, size: size, flow: flow)
            }
            .mask { Text(text).font(.system(size: fontSize, weight: .bold)) }
            .offset(x: depthPt, y: depthPt)
            .accessibilityHidden(true)

            Text(text)
                .font(.system(size: fontSize, weight: .bold))
        }
        .onAppear(perform: start)
        .accessibilityElement(children: .ignore)
        .accessibilityLabel(text)
    }

    private func hatch(in context: inout GraphicsContext, size: CGSize, flow: CGFloat) {
        // A generously oversized, rotated field of stripes so the hatch
        // still fully covers the glyphs after rotation, at any angle.
        let span = hypot(size.width, size.height) * 1.6
        let periodPt = CGFloat(stripe * 2)
        var lines = Path()
        var x = -span - flow.truncatingRemainder(dividingBy: periodPt)
        while x < span {
            lines.addRect(CGRect(x: x, y: -span / 2, width: stripe, height: span))
            x += periodPt
        }

        context.translateBy(x: size.width / 2, y: size.height / 2)
        context.rotate(by: .degrees(angle))
        context.fill(lines, with: .color(shadowColor))
    }

    private func start() {
        guard !reduceMotion else { return }
        withAnimation(.easeOut(duration: 0.4)) { grown = 1 }
        guard phase == nil else { return }
        withAnimation(.linear(duration: loopPeriod).repeatForever(autoreverses: false)) {
            animated = 1
        }
    }
}

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

SwiftUI note. The hatch is a hand-built `Canvas` field of rects rather than a tiled gradient — Canvas contents are one of the properties `withAnimation` genuinely interpolates, so this stays as cheap as `ripple-grid`.

Dependencies

SwiftUI
No external dependencies

Requires iOS 17

Flutter
No external dependencies

Requires Flutter 3.27