Holographic Card
A card with a holographic foil whose colors, glints and glare shift as you tilt it by dragging, drawn by a Metal shader.

- Cards
- iOS 18.0+
- advanced
- Metal shader
- Origin
- Original work
- Found by
- holographic · foil · shader · metal · iridescent · rainbow · card · tilt · trading card · color effect · holográfico · holograma · shader · metal · iridiscente · arcoíris · tarjeta · inclinación · cromo · efecto de color
Description
Section titled “Description”A card with a holographic foil that shifts as you tilt it by dragging, drawn by a Metal shader.
A shader draws what SwiftUI draws itself: a view backed by UIKit, such as a map, shows a placeholder.
With Reduce Motion on, the card stays flat and the foil stays still.
struct HolographicCardDemo: View { var body: some View { ZStack { LinearGradient( colors: [Color(red: 0.04, green: 0.04, blue: 0.09), Color(red: 0.14, green: 0.09, blue: 0.27)], startPoint: .top, endPoint: .bottom ) .ignoresSafeArea()
HolographicCard { card } .padding(.horizontal, 44) } }
private var card: some View { VStack(alignment: .leading, spacing: 8) { HStack { Image(systemName: "sparkles") .font(.title) .accessibilityHidden(true)
Spacer()
Text("No. 001") .font(.footnote.monospaced()) .opacity(0.7) }
Spacer()
Text("Prism Pass") .font(.system(.largeTitle, design: .rounded, weight: .bold)) Text("Drag to tilt the card") .font(.subheadline) .opacity(0.8) } .foregroundStyle(.white) .padding(24) .frame(maxWidth: .infinity, alignment: .leading) .aspectRatio(0.68, contentMode: .fit) .background( LinearGradient( colors: [ Color(red: 0.1, green: 0.12, blue: 0.3), Color(red: 0.32, green: 0.12, blue: 0.42), Color(red: 0.06, green: 0.06, blue: 0.14), ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .accessibilityElement(children: .combine) }}API reference
Section titled “API reference”| Parameter | Type | Default | Description |
|---|---|---|---|
maxAngle | Double | 16 | The degrees the card leans at the end of a drag. |
cornerRadius | CGFloat | 28 | |
intensity | Double | 1 | How much foil the card has, from 0 for none to 1. |
content | () -> Content | Required |
Copy both files into your project: the Swift file and the Metal shader it draws with. Or download HolographicCard.swift and HolographicCard.metal.
// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
import SwiftUI
nonisolated enum HolographicCardMath { /// How far the card leans for a drag, from -1 to 1 on each axis. /// Dragging half the card's size leans it all the way. static func tilt(for translation: CGSize, in size: CGSize) -> CGSize { guard size.width > 0, size.height > 0 else { return .zero } return CGSize( width: min(max(translation.width / (size.width / 2), -1), 1), height: min(max(translation.height / (size.height / 2), -1), 1) ) }
/// The degrees the card turns around its vertical axis for a lean from -1 to 1. /// `rotation3DEffect` mirrors in a right-to-left layout and a drag does not, so the turn is reversed there. static func yaw(for lean: Double, maxAngle: Double, layoutDirection: LayoutDirection) -> Double { let turn = lean * maxAngle return layoutDirection == .rightToLeft ? -turn : turn }
/// The function in `HolographicCard.metal`, with its arguments in the order it declares them. static func shader(size: CGSize, tilt: CGSize, intensity: Double) -> Shader { ShaderLibrary.HolographicCardFoil(.float2(size), .float2(tilt), .float(intensity)) }}
/// A card with a holographic foil that shifts as you tilt it by dragging, drawn by a Metal shader.////// - Important: Copy two files into the same target: `HolographicCard.swift` and `HolographicCard.metal`.////// A shader draws what SwiftUI draws itself: a view backed by UIKit, such as a map, shows a placeholder.////// With Reduce Motion on, the card stays flat and the foil stays still.struct HolographicCard<Content: View>: View { /// The degrees the card leans at the end of a drag. var maxAngle: Double = 16 var cornerRadius: CGFloat = 28 /// How much foil the card has, from 0 for none to 1. var intensity: Double = 1 @ViewBuilder var content: () -> Content
@GestureState private var tilt = CGSize.zero @State private var size = CGSize.zero @Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View { content() .onGeometryChange(for: CGSize.self) { $0.size } action: { size = $0 } .modifier( HolographicCardSurface( tilt: reduceMotion ? .zero : tilt, size: size, maxAngle: maxAngle, cornerRadius: cornerRadius, intensity: intensity ) ) .gesture(drag) .animation(.spring(duration: 0.5, bounce: 0.3), value: tilt) }
private var drag: some Gesture { DragGesture(coordinateSpace: .global) .updating($tilt) { value, tilt, _ in tilt = HolographicCardMath.tilt(for: value.translation, in: size) } }}
/// `Animatable`, so the foil follows the same spring as the lean: a shader's arguments do not animate by themselves.private struct HolographicCardSurface: ViewModifier, Animatable { var tilt: CGSize let size: CGSize let maxAngle: Double let cornerRadius: CGFloat let intensity: Double
@Environment(\.layoutDirection) private var layoutDirection
nonisolated var animatableData: CGSize.AnimatableData { get { tilt.animatableData } set { tilt.animatableData = newValue } }
func body(content: Content) -> some View { content .colorEffect(HolographicCardMath.shader(size: size, tilt: tilt, intensity: intensity)) .clipShape(.rect(cornerRadius: cornerRadius)) .shadow(color: .black.opacity(0.4), radius: 24, y: 16) .rotation3DEffect(.degrees(-tilt.height * maxAngle), axis: (x: 1, y: 0, z: 0), perspective: 0.6) .rotation3DEffect( .degrees(HolographicCardMath.yaw(for: tilt.width, maxAngle: maxAngle, layoutDirection: layoutDirection)), axis: (x: 0, y: 1, z: 0), perspective: 0.6 ) }}
// MARK: - Demo
struct HolographicCardDemo: View { var body: some View { ZStack { LinearGradient( colors: [Color(red: 0.04, green: 0.04, blue: 0.09), Color(red: 0.14, green: 0.09, blue: 0.27)], startPoint: .top, endPoint: .bottom ) .ignoresSafeArea()
HolographicCard { card } .padding(.horizontal, 44) } }
private var card: some View { VStack(alignment: .leading, spacing: 8) { HStack { Image(systemName: "sparkles") .font(.title) .accessibilityHidden(true)
Spacer()
Text("No. 001") .font(.footnote.monospaced()) .opacity(0.7) }
Spacer()
Text("Prism Pass") .font(.system(.largeTitle, design: .rounded, weight: .bold)) Text("Drag to tilt the card") .font(.subheadline) .opacity(0.8) } .foregroundStyle(.white) .padding(24) .frame(maxWidth: .infinity, alignment: .leading) .aspectRatio(0.68, contentMode: .fit) .background( LinearGradient( colors: [ Color(red: 0.1, green: 0.12, blue: 0.3), Color(red: 0.32, green: 0.12, blue: 0.42), Color(red: 0.06, green: 0.06, blue: 0.14), ], startPoint: .topLeading, endPoint: .bottomTrailing ) ) .accessibilityElement(children: .combine) }}
#Preview { HolographicCardDemo()}// SPDX-License-Identifier: MIT// Copyright (c) 2026 Oscar Canton and SwiftUI Blocks contributors
#include <metal_stdlib>#include <SwiftUI/SwiftUI_Metal.h>
using namespace metal;
// A repeatable pseudo-random number from 0 to 1 for each cell of a grid.static float HolographicCardRandom(float2 cell) { uint2 seed = uint2(int2(cell)); uint mixed = (seed.x * 0x9E3779B1u) ^ (seed.y * 0x85EBCA77u); mixed ^= mixed >> 15; mixed *= 0x2C1B3C6Du; mixed ^= mixed >> 12; return float(mixed & 0xFFFFu) / 65535.0;}
// For SwiftUI's `colorEffect`. Adds a holographic foil to each pixel of a view// of `size` points. `tilt` runs from -1 to 1 on each axis, and moves the colors// and the glare as a real card does when it turns under a light.[[ stitchable ]] half4 HolographicCardFoil(float2 position, half4 color, float2 size, float2 tilt, float intensity) { if (color.a <= 0.0) { return color; } float2 fromCenter = position / max(size, float2(1.0)) - 0.5;
// A diffraction foil changes hue with the viewing angle, so its bands of // color slide across the card as it tilts. float angle = dot(fromCenter, float2(0.9, 0.5)) * 1.4 + dot(tilt, float2(0.8, 0.6)); float3 spectrum = 0.5 + 0.5 * cos(6.2831853 * (angle + float3(0.0, 0.33, 0.67)));
// Specks of glitter, one in each cell of a grid and somewhere off its // center. Each catches the light at its own angle, so they twinkle in turn. float2 grid = position / 5.0; float2 cell = floor(grid); float2 speck = 0.5 + 0.56 * (float2(HolographicCardRandom(cell + 17.0), HolographicCardRandom(cell + 43.0)) - 0.5); float shape = saturate(1.0 - length(grid - cell - speck) / 0.22); float facing = 0.5 + 0.5 * cos(6.2831853 * (HolographicCardRandom(cell) + angle * 2.0)); float glint = smoothstep(0.55, 1.0, facing) * shape * shape;
// The glare of a light above the card slides against the tilt. float2 fromGlare = fromCenter + tilt * 0.35; float glare = exp(-6.0 * dot(fromGlare, fromGlare));
float3 foil = intensity * (spectrum * (0.14 + 0.4 * glare) + glint * (0.3 + 0.7 * glare));
// Screen the foil over the pixel, so it only ever adds light. Colors come // premultiplied by their alpha. float3 base = float3(color.rgb) / color.a; float3 lit = 1.0 - (1.0 - base) * (1.0 - saturate(foil)); return half4(half3(lit * color.a), color.a);}