
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
- Flutter
- No external dependencies
Requires iOS 17
Requires Flutter 3.27


