Skip to content
Aurora preview
Painted live in the browser. The committed still and loop are rendered from the SwiftUI source.

Aurora

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

Slow drifting colour blobs behind content. Four gradients and one blur.

Backgrounds · SwiftUI · Flutter · iOS 17 · Flutter 3.27

  • background
  • aurora
  • gradient
  • blur
  • ambient
  • animated

The actual source

AuroraBackground.swift
// Aurora · syxUI · https://syxui.dev/components/aurora-background
// Free in any project. Keep this line and credit syxUI where a person can read it.

import SwiftUI

/// A slow aurora: a few heavily blurred colour blobs drifting behind content.
///
/// Cheap by construction — four gradient ellipses and one blur, no Canvas, no
/// per-frame drawing. Drop it in a `ZStack` behind anything.
///
/// Leave `phase` nil and it drifts on its own. Pass a value in 0...1 to drive
/// the loop yourself, which is useful for scroll-linked backgrounds and makes
/// the motion deterministic.
struct AuroraBackground: View {
    var colors: [Color] = [
        Color(red: 0.45, green: 0.36, blue: 0.98),
        Color(red: 0.96, green: 0.42, blue: 0.72),
        Color(red: 0.29, green: 0.72, blue: 0.98),
        Color(red: 0.99, green: 0.72, blue: 0.42),
    ]
    var base: Color = Color(white: 0.99)
    var blurRadius: CGFloat = 90
    var period: Double = 14
    var phase: Double? = nil

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

    /// 0 at rest, 1 fully drifted. An external phase is mapped through a
    /// cosine so t = 0 and t = 1 land on the same frame and the loop closes.
    private var drift: Double {
        if let phase { return (1 - cos(phase * 2 * .pi)) / 2 }
        return animatedDrift
    }

    var body: some View {
        GeometryReader { geo in
            let side = max(geo.size.width, geo.size.height)

            ZStack {
                base
                blob(colors[0], size: side * 0.95, from: (-0.05, -0.12), to: (-0.22, -0.28), side: side)
                blob(colors[1], size: side * 0.85, from: (0.10, -0.26), to: (0.26, -0.10), side: side)
                blob(colors[2], size: side * 0.90, from: (0.04, 0.10), to: (-0.18, 0.24), side: side)
                blob(colors[3], size: side * 0.70, from: (0.30, 0.14), to: (0.20, 0.26), side: side)
            }
            .blur(radius: blurRadius)
            // Blur samples past the bounds, so clip after blurring or the
            // edges wash out.
            .clipped()
        }
        .ignoresSafeArea()
        .onAppear(perform: startDrift)
        .accessibilityHidden(true)
    }

    private func startDrift() {
        guard phase == nil, !reduceMotion else { return }
        withAnimation(.easeInOut(duration: period).repeatForever(autoreverses: true)) {
            animatedDrift = 1
        }
    }

    private func blob(
        _ color: Color,
        size: CGFloat,
        from: (CGFloat, CGFloat),
        to: (CGFloat, CGFloat),
        side: CGFloat
    ) -> some View {
        let x = from.0 + (to.0 - from.0) * drift
        let y = from.1 + (to.1 - from.1) * drift

        return Ellipse()
            .fill(
                RadialGradient(
                    colors: [color.opacity(0.85), color.opacity(0)],
                    center: .center,
                    startRadius: 0,
                    endRadius: size / 2
                )
            )
            .frame(width: size, height: size * 0.8)
            .offset(x: x * side, y: y * side)
    }
}

#Preview {
    AuroraBackground()
        .frame(width: 400, height: 300)
}
iOS 17 · No dependencies

SwiftUI note. One 14-second animation drives all four blobs. There is no Canvas and no timer, so this is safe to leave running behind a full screen.

Dependencies

SwiftUI
No external dependencies

Requires iOS 17

Flutter
No external dependencies

Requires Flutter 3.27