Tilt Card
A card that tilts in 3D toward the finger with a moving highlight.

- Cards
- iOS 18.0+
- intermediate
- Origin
- Original work
- Found by
- 3d · tilt · card · parallax · perspective · highlight · holographic · 3d · inclinación · tarjeta · perspectiva · brillo · holográfico
Description
Section titled “Description”A card that leans in 3D toward the finger, with a highlight that follows it.
restingPoint is where the card leans while nobody touches it. nil keeps it flat. With Reduce Motion on, the card stays flat and only the highlight moves.
struct TiltCardDemo: View { @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { ZStack { LinearGradient(colors: [.black, .indigo], startPoint: .top, endPoint: .bottom) .ignoresSafeArea()
TimelineView(.animation(minimumInterval: 1.0 / 60.0, paused: reduceMotion)) { context in let time = context.date.timeIntervalSinceReferenceDate * 1.6
TiltCard(restingPoint: reduceMotion ? nil : UnitPoint(x: 0.5 + 0.45 * cos(time), y: 0.5 + 0.45 * sin(time))) { card } } .padding(.horizontal, 44) } }
private var card: some View { VStack(alignment: .leading, spacing: 8) { Image(systemName: "sparkles") .font(.largeTitle) .accessibilityHidden(true)
Spacer()
Text("Aurora Pass") .font(.system(.largeTitle, design: .rounded, weight: .bold)) Text("Touch and move your finger") .font(.subheadline) .opacity(0.8) } .foregroundStyle(.white) .padding(24) .frame(maxWidth: .infinity, alignment: .leading) .aspectRatio(0.68, contentMode: .fit) .background(LinearGradient(colors: [.purple, .pink, .orange], startPoint: .topLeading, endPoint: .bottomTrailing)) .accessibilityElement(children: .combine) }}API reference
Section titled “API reference”| Parameter | Type | Default |
|---|---|---|
maxAngle | Double | 14 |
cornerRadius | CGFloat | 28 |
restingPoint | UnitPoint? | nil |
content | () -> Content | Required |
Copy this code into your project, or download the file.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
/// A card that leans in 3D toward the finger, with a highlight that follows it.////// `restingPoint` is where the card leans while nobody touches it. `nil` keeps it flat./// With Reduce Motion on, the card stays flat and only the highlight moves.struct TiltCard<Content: View>: View { var maxAngle: Double = 14 var cornerRadius: CGFloat = 28 var restingPoint: UnitPoint? @ViewBuilder var content: () -> Content
@GestureState private var touchPoint: UnitPoint? @State private var size = CGSize.zero @Environment(\.accessibilityReduceMotion) private var reduceMotion @Environment(\.layoutDirection) private var layoutDirection
private var focus: UnitPoint? { touchPoint ?? restingPoint }
private var pitch: Double { guard let focus, !reduceMotion else { return 0 } return (0.5 - focus.y) * 2 * maxAngle }
private var yaw: Double { guard let focus, !reduceMotion else { return 0 } return (focus.x - 0.5) * 2 * maxAngle }
var body: some View { content() .overlay { highlight } .clipShape(.rect(cornerRadius: cornerRadius)) .onGeometryChange(for: CGSize.self) { $0.size } action: { size = $0 } .gesture(tilt) .shadow(color: .black.opacity(0.35), radius: 24, y: 16) .rotation3DEffect(.degrees(pitch), axis: (x: 1, y: 0, z: 0), perspective: 0.6) .rotation3DEffect(.degrees(yaw), axis: (x: 0, y: 1, z: 0), perspective: 0.6) .animation(reduceMotion ? nil : .spring(duration: 0.4, bounce: 0.3), value: touchPoint) }
private var highlight: some View { RadialGradient( colors: [.white.opacity(0.35), .white.opacity(0)], center: focus ?? .center, startRadius: 0, endRadius: max(size.width, size.height) * 0.8 ) .blendMode(.plusLighter) .opacity(focus == nil ? 0 : 1) .allowsHitTesting(false) .accessibilityHidden(true) }
private var tilt: some Gesture { DragGesture(minimumDistance: 0) .updating($touchPoint) { value, state, _ in guard size.width > 0, size.height > 0 else { return } // Gestures report screen points, but a unit point counts from the leading edge. let x = min(max(value.location.x / size.width, 0), 1) state = UnitPoint( x: layoutDirection == .rightToLeft ? 1 - x : x, y: min(max(value.location.y / size.height, 0), 1) ) } }}
// MARK: - Demo
struct TiltCardDemo: View { @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { ZStack { LinearGradient(colors: [.black, .indigo], startPoint: .top, endPoint: .bottom) .ignoresSafeArea()
TimelineView(.animation(minimumInterval: 1.0 / 60.0, paused: reduceMotion)) { context in let time = context.date.timeIntervalSinceReferenceDate * 1.6
TiltCard(restingPoint: reduceMotion ? nil : UnitPoint(x: 0.5 + 0.45 * cos(time), y: 0.5 + 0.45 * sin(time))) { card } } .padding(.horizontal, 44) } }
private var card: some View { VStack(alignment: .leading, spacing: 8) { Image(systemName: "sparkles") .font(.largeTitle) .accessibilityHidden(true)
Spacer()
Text("Aurora Pass") .font(.system(.largeTitle, design: .rounded, weight: .bold)) Text("Touch and move your finger") .font(.subheadline) .opacity(0.8) } .foregroundStyle(.white) .padding(24) .frame(maxWidth: .infinity, alignment: .leading) .aspectRatio(0.68, contentMode: .fit) .background(LinearGradient(colors: [.purple, .pink, .orange], startPoint: .topLeading, endPoint: .bottomTrailing)) .accessibilityElement(children: .combine) }}
#Preview { TiltCardDemo()}