Skip to content
Orbit Loader preview
An animated render of the SwiftUI source on this page.

Orbit Loader

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

Three dots orbiting a faint ring at different radii and speeds.

Loaders · SwiftUI · Flutter · iOS 17 · Flutter 3.27

  • loader
  • loading
  • spinner
  • orbit
  • dots
  • animated

The actual source

OrbitLoader.swift
// Orbit Loader · syxUI · https://syxui.dev/components/orbit-loader
// Free in any project. Keep this line and credit syxUI where a person can read it.

import SwiftUI

/// Dots orbiting a faint ring at different radii and speeds.
///
/// Reads as motion even at 20pt, where a spinner arc just looks like a smudge.
struct OrbitLoader: View {
    var size: CGFloat = 64
    var color: Color = .black
    var period: Double = 1.6
    var phase: Double? = nil

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

    private var current: Double { phase ?? animated }

    /// radius as a fraction of the box, dot diameter, speed multiplier
    private let orbits: [(radius: Double, dot: Double, speed: Double)] = [
        (0.42, 0.115, 1.0),
        (0.27, 0.085, -1.6),
        (0.13, 0.065, 2.4),
    ]

    var body: some View {
        Canvas { context, canvasSize in
            let centre = CGPoint(x: canvasSize.width / 2, y: canvasSize.height / 2)
            let side = min(canvasSize.width, canvasSize.height)

            context.stroke(
                Path(ellipseIn: CGRect(
                    x: centre.x - side * 0.42,
                    y: centre.y - side * 0.42,
                    width: side * 0.84,
                    height: side * 0.84
                )),
                with: .color(color.opacity(0.12)),
                lineWidth: 1
            )

            for orbit in orbits {
                let angle = (current * orbit.speed) * 2 * .pi
                let point = CGPoint(
                    x: centre.x + cos(angle) * side * orbit.radius,
                    y: centre.y + sin(angle) * side * orbit.radius
                )
                let diameter = side * orbit.dot

                context.fill(
                    Path(ellipseIn: CGRect(
                        x: point.x - diameter / 2,
                        y: point.y - diameter / 2,
                        width: diameter,
                        height: diameter
                    )),
                    with: .color(color)
                )
            }
        }
        .frame(width: size, height: size)
        .onAppear(perform: start)
        .accessibilityElement()
        .accessibilityLabel("Loading")
    }

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

#Preview {
    OrbitLoader()
        .padding(40)
}
iOS 17 · No dependencies

SwiftUI note. One Canvas, four fills. The middle dot runs backwards, which is what stops the group reading as a single rotating object.

Dependencies

SwiftUI
No external dependencies

Requires iOS 17

Flutter
No external dependencies

Requires Flutter 3.27