SwiftUI Skeleton Loading View: Build a Shimmer Effect from Scratch

If you’ve used apps like Twitter, Youtube or LinkedIn while they’re loading content, you’ve seen the SwiftUI skeleton screen effect — that smooth, animated highlight that sweeps across placeholder shapes.

It’s one of the best ways to give users visual feedback while data loads, replacing static blank screens with a polished SwiftUI placeholder view that feels alive.

In this post, I’ll walk you through a quick guide on how to build it from scratch in SwiftUI.

Project Setup

Start by creating a new SwiftUI file. Right-click in your project navigator, choose New File → SwiftUI View, and name it ShimmerView (or whatever you prefer). This will be our reusable component.

Step 1: Setting Up the Gray Placeholder Background

The base of the shimmer effect is a gray placeholder background. Add a gray color with a low opacity to keep it easy on the eyes:

Color.gray
    .opacity(0.2)

That’s all there is to the background — simple and clean.

Step 2: Building the LinearGradient Shimmer Effect

The shimmer is a linear gradient that fades in from the left, peaks in the middle, and fades back out on the right. We place it on top of the background using the .overlay modifier.

Because we need to know the full width of the view for the animation, we wrap everything inside a GeometryReader:

.overlay(
    GeometryReader { reader in
        LinearGradient(
            colors: [
                .clear,
                .gray.opacity(0.3),
                .clear
            ],
            startPoint: .leading,
            endPoint: .trailing
        )
        .frame(width: 200)
    }
)

The three-color gradient — clear → gray → clear — gives us that soft, fading highlight. We constrain its width to 200 points so it appears as a stripe rather than filling the whole view. Feel free to adjust this to taste.

Step 3: Animating the Shimmer with KeyframeAnimator

Now for the fun part: making it move. We use SwiftUI’s keyframeAnimator modifier to sweep the gradient from left to right on a continuous loop.

.keyframeAnimator(
    initialValue: 0.0,
    repeating: true
) { content, value in
    content
        .offset(x: value)
} keyframes: { _ in
    LinearKeyframe(
        reader.size.width + 200,
        duration: 1.5
    )
}

A few things to note here:

  • initialValue: 0.0 — the animation starts at x = 0.
  • repeating: true — the animation loops continuously as long as the view is on screen.
  • LinearKeyframe — this gives us a constant, even speed across the animation (no easing in or out), which is exactly the effect we want.
  • Duration of 1.5 seconds — a smooth, natural pace. Adjust to your preference.

Step 4: Finishing Touches

You’ll notice the gradient starts in the middle of the view rather than offscreen to the left. Fix this by applying a negative x-offset to the gradient so it begins fully out of frame:

.offset(x: -200)

Now the shimmer sweeps all the way from left to right — exactly as expected.

How to Apply the Shimmer to Any SwiftUI View

Head over to ContentView (or wherever you want to use it) and apply ShimmerView as a background to any SwiftUI placeholder view using .clipShape:

VStack {
    // Rectangle placeholder (e.g. for a banner or text line)
    ShimmerView()
        .clipShape(RoundedRectangle(cornerRadius: 8))
        .frame(height: 60)

    // Circle placeholder (e.g. for an avatar)
    ShimmerView()
        .clipShape(Circle())
        .frame(width: 60, height: 60)
}
.padding()

By combining ShimmerView with different clip shapes and frame sizes, you can mock out any kind of loading UI — profile pictures, text lines, cards, and more.

Using the Shimmer as a SwiftUI ViewModifier

The ShimmerView approach works well, but there’s a more flexible and idiomatic SwiftUI way to do it — turning the shimmer into a ViewModifier. This lets you apply the effect to any existing view with a single modifier call, rather than wrapping things in a dedicated component.

Here’s the full implementation:

struct ShimmerModifier: ViewModifier {
    var backgroundColor: Color
    
    func body(content: Content) -> some View {
        content
            .overlay {
                GeometryReader { geometry in
                    LinearGradient(
                        colors: [.clear, .gray.opacity(0.2), .clear],
                        startPoint: .leading,
                        endPoint: .trailing
                    )
                    .frame(width: 200)
                    .offset(x: -200)
                    .keyframeAnimator(initialValue: 0.0, repeating: true) { shimmerContent, value in
                        shimmerContent.offset(x: value)
                    } keyframes: { _ in
                        LinearKeyframe(geometry.size.width + 200, duration: 1.5)
                    }
                }
            }
            .background(backgroundColor)
    }
}

extension View {
    func shimmer(
        backgroundColor: Color = .gray.opacity(0.2)
    ) -> some View {
        modifier(
            ShimmerModifier(backgroundColor: backgroundColor)
        )
    }
}

A couple of things worth highlighting:

  • backgroundColor parameter — the background color is now part of the modifier itself, with a sensible default of gray.opacity(0.2). This means you no longer need to set the background separately on each placeholder shape.
  • extension View — the convenience extension gives you clean dot-syntax across your entire app, just like any native SwiftUI modifier.

Usage is now much cleaner. Apply .shimmer() directly to any shape or view:

VStack(spacing: 16) {
    // Text line placeholder
    RoundedRectangle(cornerRadius: 8)
        .shimmer()
        .frame(height: 16)

    // Avatar placeholder
    Circle()
        .shimmer(backgroundColor: .gray.opacity(0.3))
        .frame(width: 60, height: 60)

    // Banner placeholder
    RoundedRectangle(cornerRadius: 12)
        .shimmer()
        .frame(height: 120)
}
.padding()

Notice how the shape itself defines the skeleton, and .shimmer() just layers the animation on top. This separation of concerns makes it trivial to swap placeholder shapes or customize colors per use case.

Summary

Here’s the full picture of what we built:

  1. Gray background with low opacity as the base layer.
  2. Linear gradient overlay fading clear → gray → clear, constrained to 200pt wide.
  3. keyframeAnimator sweeping the gradient from left to right at constant speed.
  4. Negative initial offset so the animation starts fully offscreen.
  5. Reusable component you clip into any shape you need.

It’s a small touch, but a SwiftUI skeleton screen makes a big difference in how polished your app feels. Give it a try and tweak the width, duration, and colors to match your design.

Scroll to Top