Claude
Skills
Sign in
Back

progressive-blur-header-swiftui

Included with Lifetime
$97 forever

Drop-in SwiftUI sticky header component with progressive blur effect matching Apple Music, Photos, and App Store styles.

Design

What this skill does


# ProgressiveBlurHeader SwiftUI Skill

> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.

A drop-in SwiftUI package for sticky headers with progressive (variable-radius) blur — replicating the Apple Music, Photos, and App Store style where content scrolls underneath the header with increasing blur and tint. No clipping, no hard edges.

## Installation

### Swift Package Manager (Xcode)

**File → Add Package Dependencies** → paste:
```
https://github.com/dominikmartn/ProgressiveBlurHeader
```
Select branch: `main`

### Package.swift

```swift
dependencies: [
    .package(url: "https://github.com/dominikmartn/ProgressiveBlurHeader", branch: "main"),
]
```

Then add to your target:
```swift
.target(
    name: "YourApp",
    dependencies: ["ProgressiveBlurHeader"]
)
```

**Requirements:** iOS 16+, Swift 5.9+

---

## Core API

### `StickyBlurHeader`

The primary component. Takes two `ViewBuilder` closures: `header` and `content`.

```swift
StickyBlurHeader(
    maxBlurRadius: Double,     // Default: 5
    fadeExtension: CGFloat,    // Default: 64
    tintOpacityTop: Double,    // Default: 0.7
    tintOpacityMiddle: Double  // Default: 0.5
) {
    // header view (NO opaque background)
} content: {
    // scrollable content
}
```

### Parameters

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `maxBlurRadius` | `Double` | `5` | Max blur at the top edge. 5 = subtle, 10 = moderate, 20 = strong |
| `fadeExtension` | `CGFloat` | `64` | Points below the header the blur extends |
| `tintOpacityTop` | `Double` | `0.7` | Tint behind Dynamic Island / status bar |
| `tintOpacityMiddle` | `Double` | `0.5` | Tint at the header's vertical center |

The tint color adapts automatically to light/dark mode.

---

## Basic Usage

```swift
import SwiftUI
import ProgressiveBlurHeader

struct ContentView: View {
    let items = (1...50).map { "Item \($0)" }

    var body: some View {
        StickyBlurHeader {
            // Header — NO opaque background
            HStack {
                Button("Back") { }
                Spacer()
                Text("Library").font(.headline)
                Spacer()
                Button("Settings") { }
            }
            .padding()
        } content: {
            ForEach(items, id: \.self) { item in
                Text(item)
                    .frame(maxWidth: .infinity, alignment: .leading)
                    .padding()
                    .background(Color(.secondarySystemBackground))
                    .cornerRadius(8)
                    .padding(.horizontal)
            }
        }
        .background(Color(.systemBackground))
    }
}
```

---

## Common Patterns

### Navigation-Style Header with Back Button

```swift
import SwiftUI
import ProgressiveBlurHeader

struct AlbumDetailView: View {
    let albumTitle: String
    @Environment(\.dismiss) private var dismiss

    var body: some View {
        StickyBlurHeader(
            maxBlurRadius: 8,
            fadeExtension: 80,
            tintOpacityTop: 0.75,
            tintOpacityMiddle: 0.55
        ) {
            HStack {
                Button {
                    dismiss()
                } label: {
                    Image(systemName: "chevron.left")
                        .fontWeight(.semibold)
                }
                Spacer()
                Text(albumTitle)
                    .font(.headline)
                    .lineLimit(1)
                Spacer()
                Button {
                    // action
                } label: {
                    Image(systemName: "ellipsis.circle")
                }
            }
            .padding(.horizontal)
            .padding(.vertical, 12)
        } content: {
            LazyVStack(spacing: 0) {
                ForEach(0..<30) { index in
                    SongRow(index: index)
                    Divider().padding(.leading)
                }
            }
        }
        .background(Color(.systemBackground))
    }
}
```

### Subtle Blur (Apple Photos Style)

```swift
StickyBlurHeader(
    maxBlurRadius: 5,
    fadeExtension: 56,
    tintOpacityTop: 0.6,
    tintOpacityMiddle: 0.4
) {
    HStack {
        Text("Photos")
            .font(.largeTitle)
            .fontWeight(.bold)
        Spacer()
        Button {
            // action
        } label: {
            Image(systemName: "plus")
        }
    }
    .padding(.horizontal)
    .padding(.top, 8)
    .padding(.bottom, 12)
} content: {
    LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: 2) {
        ForEach(photos) { photo in
            PhotoThumbnail(photo: photo)
        }
    }
    .padding(2)
}
.background(Color(.systemBackground))
```

### Strong Blur (App Store Style)

```swift
StickyBlurHeader(
    maxBlurRadius: 12,
    fadeExtension: 72,
    tintOpacityTop: 0.85,
    tintOpacityMiddle: 0.6
) {
    VStack(spacing: 4) {
        HStack {
            Text("Today")
                .font(.largeTitle)
                .fontWeight(.bold)
            Spacer()
            Button {
                // profile action
            } label: {
                Image(systemName: "person.crop.circle.fill")
                    .font(.title2)
            }
        }
        HStack {
            Text(Date(), style: .date)
                .font(.subheadline)
                .foregroundStyle(.secondary)
                .textCase(.uppercase)
            Spacer()
        }
    }
    .padding(.horizontal)
    .padding(.vertical, 12)
} content: {
    LazyVStack(spacing: 16) {
        ForEach(featuredApps) { app in
            AppCard(app: app)
        }
    }
    .padding()
}
.background(Color(.systemBackground))
```

### Dynamic Header (Height Changes)

Header height is measured automatically via `GeometryReader` + `PreferenceKey` — no manual sizing needed:

```swift
StickyBlurHeader {
    VStack(alignment: .leading, spacing: 4) {
        Text("Title").font(.headline)
        if showsSubtitle {
            Text("Subtitle").font(.subheadline).foregroundStyle(.secondary)
        }
    }
    .padding()
    // Header auto-adjusts when showsSubtitle toggles
} content: {
    contentList
}
.background(Color(.systemBackground))
```

---

## Architecture

The component uses a three-layer `ZStack`:

| Layer | Component | Purpose |
|-------|-----------|---------|
| Back | `ScrollView` | Content scrolls freely, never clipped |
| Middle | `VariableBlurView` + gradient | Progressive blur + adaptive tint |
| Front | Your header view | Floats above blur, transparent background |

The blur engine is [VariableBlur by nikstar](https://github.com/nikstar/VariableBlur), which uses the same private API Apple uses internally. It is App Store approved.

---

## Critical Rules

### ❌ Never add an opaque background to the header
```swift
// WRONG — hides the blur effect
StickyBlurHeader {
    Text("Header")
        .background(Color.white) // ❌ Breaks the blur
}

// CORRECT — no background on header views
StickyBlurHeader {
    Text("Header") // ✅ Transparent, blur shows through
}
```

### ❌ Never clip the content hierarchy
```swift
// WRONG
StickyBlurHeader { ... } content: {
    VStack { ... }
        .clipped() // ❌ Content becomes invisible under header
}

// CORRECT — let content remain visible under blur
StickyBlurHeader { ... } content: {
    VStack { ... } // ✅ No clipping
}
```

### ✅ Always set a background on the outer container
```swift
StickyBlurHeader { ... } content: { ... }
    .background(Color(.systemBackground)) // ✅ Required for tint to work correctly
```

---

## Troubleshooting

### Blur not visible / header looks opaque
- Remove any `.background(...)` modifier from your header view
- Ensure no parent view adds a background before `StickyBlurHeader`

### Content disappears under header
- Remove any `.clipped()` from the content or its ancestors
- Do not wrap content in a `ScrollView` — `StickyBlurHeader` provides its own

### Header height is wrong
- Do not set explicit height

Related in Design