Skip to content
Conic Rim preview
An animated render of the SwiftUI source on this page.

Conic Rim

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

A card ringed by a rotating conic gradient, one bright comet head circling the border.

Cards · SwiftUI · Flutter · iOS 17 · Flutter 3.27

  • card
  • border
  • conic
  • gradient
  • glow
  • animated
  • loop

The actual source

DepthConicRim.swift
// Conic Rim · syxUI · https://syxui.dev/components/depth-conic-rim
// Free in any project. Keep this line and credit syxUI where a person can read it.

import SwiftUI

/// A card ringed by a rotating conic gradient — one bright comet head circling
/// the border with a long tail fading out behind it.
///
/// The entire effect is a single `Double`: the angle the `AngularGradient`
/// starts at. No geometry moves and nothing re-lays out, so it costs one
/// gradient rasterisation per frame regardless of card size. Built for dark
/// surfaces — over white the rim has nothing to glow against.
struct DepthConicRim: View {
    var accentA: Color = Color(red: 0.431, green: 0.906, blue: 0.976)
    var accentB: Color = Color(red: 0.659, green: 0.333, blue: 0.969)
    var surface: Color = Color(red: 0.043, green: 0.051, blue: 0.078)
    var borderWidth: CGFloat = 1.5
    var glowRadius: CGFloat = 12
    /// Seconds for one full turn of the rim.
    var period: Double = 4
    /// 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
    @State private var animated: Double = 0

    private var current: Double { phase ?? animated }

    private let corner: CGFloat = 24

    private var shape: RoundedRectangle {
        RoundedRectangle(cornerRadius: corner, style: .continuous)
    }

    /// A short bright arc leading a long fade to nothing.
    ///
    /// Weighting the stops rather than spacing them evenly is the whole trick:
    /// evenly spaced stops read as a spinning rainbow, this reads as one
    /// travelling head. The last two stops are both `.clear` so three quarters
    /// of the ring stays dark and the head has somewhere to arrive from.
    private var stops: [Gradient.Stop] {
        [
            .init(color: accentA, location: 0),
            .init(color: accentB, location: 0.08),
            .init(color: accentB.opacity(0.15), location: 0.28),
            .init(color: .clear, location: 0.55),
            .init(color: .clear, location: 1),
        ]
    }

    var body: some View {
        // Linear, never eased: any easing on a full-turn loop reads as a
        // stutter every time the cycle wraps.
        let rim = AngularGradient(
            stops: stops,
            center: .center,
            angle: .degrees(360 * current)
        )

        content
            .padding(22)
            .background {
                shape.fill(surface)
                // Interior lift: a 6% wash toward the top edge so the card is
                // not a flat black rectangle sitting inside a bright ring.
                shape.fill(
                    LinearGradient(
                        colors: [.white.opacity(0.06), .clear],
                        startPoint: .top,
                        endPoint: .center
                    )
                )
            }
            .overlay {
                shape.strokeBorder(rim, lineWidth: borderWidth)
            }
            .background {
                // Bloom: the same stroke, fatter and blurred, sitting behind
                // the opaque surface so only the outer half shows.
                shape
                    .strokeBorder(rim, lineWidth: borderWidth * 2.2)
                    .blur(radius: glowRadius)
                    .opacity(0.55)
            }
            .onAppear(perform: start)
            .accessibilityElement(children: .combine)
    }

    private var content: some View {
        VStack(alignment: .leading, spacing: 18) {
            HStack(spacing: 13) {
                RoundedRectangle(cornerRadius: 13, style: .continuous)
                    .fill(
                        LinearGradient(
                            colors: [accentA, accentB],
                            startPoint: .topLeading,
                            endPoint: .bottomTrailing
                        )
                    )
                    .frame(width: 44, height: 44)
                    .overlay {
                        Image(systemName: "waveform")
                            .font(.system(size: 19, weight: .semibold))
                            .foregroundStyle(Color(white: 0.05))
                    }

                VStack(alignment: .leading, spacing: 3) {
                    Text("Neural sync")
                        .font(.system(size: 17, weight: .semibold))
                        .foregroundStyle(.white)
                    Text("4 channels · 96 kHz")
                        .font(.system(size: 12.5))
                        .foregroundStyle(.white.opacity(0.45))
                }

                Spacer(minLength: 8)

                Text("LIVE")
                    .font(.system(size: 10, weight: .bold))
                    .tracking(0.8)
                    .foregroundStyle(accentA)
                    .padding(.horizontal, 8)
                    .padding(.vertical, 4)
                    .background { Capsule().fill(accentA.opacity(0.14)) }
            }

            HStack(spacing: 0) {
                stat("18.4", "ms latency")
                stat("99.2", "% uptime")
                stat("2.1k", "packets")
            }
        }
        .frame(width: 268, alignment: .leading)
    }

    private func stat(_ value: String, _ label: String) -> some View {
        VStack(alignment: .leading, spacing: 2) {
            Text(value)
                .font(.system(size: 15, weight: .semibold, design: .rounded))
                .foregroundStyle(.white.opacity(0.92))
            Text(label)
                .font(.system(size: 10.5))
                .foregroundStyle(.white.opacity(0.38))
        }
        .frame(maxWidth: .infinity, alignment: .leading)
    }

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

#Preview {
    DepthConicRim()
        .frame(width: 380, height: 240)
        .background(Color(white: 0.02))
}
iOS 17 · No dependencies

SwiftUI note. The rotation must stay .linear — easing a full-turn loop reads as a stutter every time it wraps. The bloom is a second strokeBorder behind an opaque surface, so drop glowRadius to 0 before you reach for anything else if the card is on a scroll hot path.

Dependencies

SwiftUI
No external dependencies

Requires iOS 17

Flutter
No external dependencies

Requires Flutter 3.27