Grid Pulse Background
A grid of dots with a pulse of light spreading out from the center.

- Backgrounds
- iOS 18.0+
- intermediate
- Origin
- Original work
- Found by
- background · grid · dots · pulse · radar · ripple · animated · fondo · cuadrícula · puntos · pulso · radar · onda · animado
Description
Section titled “Description”A grid of dots with a pulse of light spreading from the center.
With Reduce Motion on, it shows one still ring.
struct GridPulseBackgroundDemo: View { var body: some View { ZStack { Color(red: 0.03, green: 0.05, blue: 0.1) .ignoresSafeArea()
GridPulseBackground()
VStack(spacing: 10) { Image(systemName: "dot.radiowaves.left.and.right") .font(.largeTitle) .accessibilityHidden(true) Text("Signal") .font(.system(.largeTitle, design: .rounded, weight: .bold)) } .foregroundStyle(.white) .padding(.horizontal, 28) .padding(.vertical, 20) .background(.ultraThinMaterial, in: .rect(cornerRadius: 24)) } }}API reference
Section titled “API reference”| Parameter | Type | Default | Description |
|---|---|---|---|
color | Color | .cyan | |
spacing | Double | 26 | |
period | Double | 2.6 | Seconds for the pulse to travel from the center to the corners. |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
enum GridPulseBackgroundMath { /// How many dots fit on each side of the center, or `nil` when `spacing` cannot lay out a grid. static func dotsPerSide(extent: Double, spacing: Double) -> Int? { guard spacing.isFinite, spacing > 0 else { return nil } return Int(extent / 2 / spacing) + 1 }}
/// A grid of dots with a pulse of light spreading from the center.////// With Reduce Motion on, it shows one still ring.struct GridPulseBackground: View { var color: Color = .cyan var spacing: Double = 26 /// Seconds for the pulse to travel from the center to the corners. var period: Double = 2.6
@Environment(\.accessibilityReduceMotion) private var reduceMotion
private static let dotRadius = 2.0 private static let restingOpacity = 0.32 private static let bandWidth = 80.0
var body: some View { TimelineView(.animation(paused: reduceMotion)) { timeline in Canvas { context, size in let cycle = timeline.date.timeIntervalSinceReferenceDate / period let progress = reduceMotion ? 0.4 : cycle - floor(cycle) draw(progress: progress, in: context, size: size) } } .ignoresSafeArea() .allowsHitTesting(false) .accessibilityHidden(true) }
private func draw(progress: Double, in context: GraphicsContext, size: CGSize) { guard let columns = GridPulseBackgroundMath.dotsPerSide(extent: size.width, spacing: spacing), let rows = GridPulseBackgroundMath.dotsPerSide(extent: size.height, spacing: spacing) else { return }
let center = CGPoint(x: size.width / 2, y: size.height / 2) let front = progress * (hypot(size.width, size.height) / 2 + Self.bandWidth) var resting = Path()
for row in -rows...rows { for column in -columns...columns { let dot = CGPoint(x: center.x + Double(column) * spacing, y: center.y + Double(row) * spacing) let gap = (hypot(dot.x - center.x, dot.y - center.y) - front) / Self.bandWidth let glow = exp(-gap * gap) * (1 - pow(progress, 4))
if glow < 0.02 { resting.addEllipse(in: circle(at: dot, radius: Self.dotRadius)) } else { context.fill( Path(ellipseIn: circle(at: dot, radius: Self.dotRadius * (1 + glow * 1.6))), with: .color(color.opacity(Self.restingOpacity + glow * (1 - Self.restingOpacity))) ) } } }
context.fill(resting, with: .color(color.opacity(Self.restingOpacity))) }
private func circle(at center: CGPoint, radius: Double) -> CGRect { CGRect(x: center.x - radius, y: center.y - radius, width: radius * 2, height: radius * 2) }}
// MARK: - Demo
struct GridPulseBackgroundDemo: View { var body: some View { ZStack { Color(red: 0.03, green: 0.05, blue: 0.1) .ignoresSafeArea()
GridPulseBackground()
VStack(spacing: 10) { Image(systemName: "dot.radiowaves.left.and.right") .font(.largeTitle) .accessibilityHidden(true) Text("Signal") .font(.system(.largeTitle, design: .rounded, weight: .bold)) } .foregroundStyle(.white) .padding(.horizontal, 28) .padding(.vertical, 20) .background(.ultraThinMaterial, in: .rect(cornerRadius: 24)) } }}
#Preview { GridPulseBackgroundDemo()}