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

Neon Text

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

A neon tube with a slow hum and the occasional stutter, like real glass.

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

  • text
  • neon
  • glow
  • flicker
  • sign
  • dark
  • animation

The actual source

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

import SwiftUI

/// A neon tube with a slow hum and the occasional stutter, like real glass.
///
/// Four stacked shadow falloffs read as glass and gas rather than a drop
/// shadow. Brightness is driven by a slow sine hum plus a hashed, stepped
/// flicker so it never repeats identically — a `TimelineView` samples both
/// every frame since neither is animatable through `withAnimation`.
struct NeonText: View {
    var text: String = "OPEN"
    var tube: Color = Color(red: 1.000, green: 0.176, blue: 0.584)
    var flicker: Double = 0.45
    /// Multiplies all four shadow radii.
    var glow: Double = 1.0
    var hum: Bool = true
    var background: Color = Color(red: 0.043, green: 0.043, blue: 0.071)
    /// Preview override. When set, the component renders that point of one loop
    /// instead of driving its own clock.
    var phase: Double? = nil

    @Environment(\.accessibilityReduceMotion) private var reduceMotion

    private let humPeriod: Double = 3.1
    private let bucketMs: Double = 80
    private let averageDipEveryMs: Double = 2600

    var body: some View {
        ZStack {
            background
            if reduceMotion && phase == nil {
                tube(brightness: 1)
            } else {
                TimelineView(.animation) { context in
                    let seconds = phase.map { $0 * humPeriod }
                        ?? context.date.timeIntervalSinceReferenceDate
                    tube(brightness: brightness(at: seconds))
                }
            }
        }
        .accessibilityElement(children: .ignore)
        .accessibilityLabel(text)
    }

    @ViewBuilder
    private func tube(brightness b: Double) -> some View {
        Text(text)
            .font(.system(size: 44, weight: .bold, design: .rounded))
            .foregroundStyle(.white.opacity(0.94 * b))
            .shadow(color: tube.opacity(0.90 * b), radius: 2 * glow)
            .shadow(color: tube.opacity(0.70 * b), radius: 6 * glow)
            .shadow(color: tube.opacity(0.45 * b), radius: 16 * glow)
            .shadow(color: tube.opacity(0.25 * b), radius: 38 * glow)
            .overlay {
                Text(text)
                    .font(.system(size: 44, weight: .bold, design: .rounded))
                    .foregroundStyle(tube)
                    .blur(radius: 12 * glow)
                    .blendMode(.plusLighter)
                    .opacity(b)
            }
            .compositingGroup()
    }

    /// Slow sine hum plus a hashed, stepped flicker dip. The flicker bucket is
    /// a pure function of time so a preview can sample any frame and still
    /// land inside a representative dip.
    private func brightness(at seconds: Double) -> Double {
        let humValue = hum ? sin(seconds / humPeriod * 2 * .pi) * 0.06 : 0
        var value = 1.0 + humValue

        guard flicker > 0 else { return value }

        let bucket = floor(seconds * 1000 / bucketMs)
        let dipsPerBucket = bucketMs / averageDipEveryMs
        let roll = hash(bucket)
        if roll < dipsPerBucket * flicker * 4 {
            // Two dip depths, chosen by a second, independent hash so the
            // stutter reads as glass arcing rather than a metronome.
            let depth = hash(bucket + 1000) < 0.5 ? 0.35 : 0.62
            value = depth
        }
        return value
    }

    /// Deterministic pseudo-random value in `0..<1` from an integer bucket —
    /// no `Int` seed needed, so previews reproduce exactly.
    private func hash(_ n: Double) -> Double {
        let x = sin(n * 12.9898) * 43758.5453
        return x - floor(x)
    }
}

#Preview {
    NeonText()
        .frame(width: 320, height: 160)
}
iOS 17 · No dependencies

SwiftUI note. Four chained `.shadow` modifiers plus one `.plusLighter` overlay is the whole cost — no offscreen pass beyond what `.compositingGroup()` already does. Push `glow` down before touching anything else if it needs to run on an older device.

Dependencies

SwiftUI
No external dependencies

Requires iOS 17

Flutter
No external dependencies

Requires Flutter 3.27