SwiftUI Tutorial

SwiftUI Card View: A Reusable Card Component with a Custom CardStyle

SwiftUI has no Card type, so you build one — a rounded, shadowed container you can reuse everywhere. This guide covers backgrounds, clipping, a CardStyle ViewModifier, making cards tappable with NavigationLink, and iOS 26 Liquid Glass cards. Real Swift 6 code.

Last updated: 2026-07-17 9 min read By Ahmed Gagan, iOS Engineer
Quick Answer

SwiftUI has no built-in Card, so wrap content in padding, add a rounded background with .background(_:in:), and clip with .clipShape(RoundedRectangle(cornerRadius:)). Package the look as a ViewModifier (a CardStyle) and expose it via a View extension so every card stays consistent. Make cards tappable by wrapping them in a NavigationLink, and on iOS 26 use .glassEffect() for Liquid Glass cards.

No built-in type
Build cards from a container plus background, clip, and shadow
Reuse
A ViewModifier + View extension keeps every card identical
iOS 26
.glassEffect() turns a card into Liquid Glass
Swift Kit support
The Swift Kit ships a themeable Card component tied to the design system's tokens for radius, shadow, and surface color.

There is no Card — you compose one

Unlike UIKit's absence of a card control, SwiftUI's answer is composition: a card is just content with padding, a rounded background, and a shadow. Start by padding your content, then apply a background using the shape-based overload .background(_:in:) which fills a RoundedRectangle behind the content in one call. Add a subtle shadow to lift the surface off the page. The order of modifiers matters — apply padding before the background so the fill wraps the padded content, not just the text. Getting this base right once is what lets you promote it into a reusable style in the next step rather than copy-pasting the same three modifiers everywhere.

A card composed inline
struct InlineCard: View {
    var body: some View {
        VStack(alignment: .leading, spacing: 8) {
            Text("Weekly Summary").font(.headline)
            Text("You completed 12 of 15 tasks.")
                .font(.subheadline)
                .foregroundStyle(.secondary)
        }
        .padding()
        .background(Color(.secondarySystemBackground),
                    in: RoundedRectangle(cornerRadius: 16))
        .shadow(color: .black.opacity(0.08), radius: 8, y: 4)
    }
}

clipShape and why it matters

The shape-based .background(_:in:) clips the fill to the rounded rectangle for you, but the moment a card contains an image that should reach the edges — a photo header, a colored banner — you need .clipShape on the whole card so the image corners round too. Apply .clipShape(RoundedRectangle(cornerRadius:)) to the container after laying out content; anything overflowing the shape gets masked. A classic bug is a card with rounded corners but a square photo poking out the top: that's a missing clipShape on the outer container. If you also want a border, add an .overlay with a stroked RoundedRectangle of the same radius so the outline follows the clip exactly.

Clipping an image-topped card
struct PhotoCard: View {
    let imageName: String
    let title: String
    var body: some View {
        VStack(alignment: .leading, spacing: 0) {
            Image(imageName)
                .resizable().scaledToFill()
                .frame(height: 140)
            Text(title).font(.headline).padding()
        }
        .background(Color(.secondarySystemBackground))
        .clipShape(RoundedRectangle(cornerRadius: 16))
        .overlay(RoundedRectangle(cornerRadius: 16)
            .stroke(.black.opacity(0.06), lineWidth: 1))
    }
}

A reusable CardStyle ViewModifier

Copy-pasting padding, background, clip, and shadow across dozens of views is how a design drifts. Package the look as a ViewModifier and expose it through a View extension so applying a card is a single .cardStyle() call. Centralizing it means changing the corner radius or shadow in one place updates every card in the app — the whole point of a design system. Parameterize the modifier if you need variants (a compact card, an elevated card) by adding stored properties. This is also where you'd reference shared tokens for surface color and radius so cards match buttons, sheets, and list rows without anyone eyeballing values.

CardStyle as a ViewModifier + extension
struct CardStyle: ViewModifier {
    var cornerRadius: CGFloat = 16
    func body(content: Content) -> some View {
        content
            .padding()
            .background(Color(.secondarySystemBackground),
                        in: RoundedRectangle(cornerRadius: cornerRadius))
            .shadow(color: .black.opacity(0.08), radius: 8, y: 4)
    }
}

extension View {
    func cardStyle(cornerRadius: CGFloat = 16) -> some View {
        modifier(CardStyle(cornerRadius: cornerRadius))
    }
}

// Usage:
// VStack { /* content */ }.cardStyle()

iOS 26 Liquid Glass cards

On iOS 26, cards can adopt the Liquid Glass material with the .glassEffect() modifier, which renders a translucent, refractive surface that picks up the content behind it — ideal for cards floating over imagery or a colorful background. Apply it in place of an opaque fill; you can pass a shape so the glass follows your rounded rectangle, and combine cards in a GlassEffectContainer to let adjacent glass elements blend naturally. Provide a graceful fallback for iOS 16–25 by branching on availability and using your material or solid background there. Keep text legible — glass over a busy backdrop can reduce contrast, so lean on vibrant foreground styles or a subtle scrim behind the text.

  • .glassEffect(in:) applies Liquid Glass clipped to your card shape
  • Group related glass cards in a GlassEffectContainer to blend
  • Branch on #available so iOS 16–25 fall back to a material
  • Watch contrast: glass over busy imagery can wash out text
A Liquid Glass card with a fallback
struct GlassCard<Content: View>: View {
    @ViewBuilder var content: Content
    var body: some View {
        let shape = RoundedRectangle(cornerRadius: 20)
        Group {
            if #available(iOS 26, *) {
                content.padding().glassEffect(in: shape)
            } else {
                content.padding()
                    .background(.ultraThinMaterial, in: shape)
            }
        }
    }
}

Ship your SwiftUI app in 5 emails

A free 5-part course on the parts that actually stall launches: paywall, auth, onboarding, App Store review, and pricing. No fluff, unsubscribe anytime.

A card component wired to your theme

The Swift Kit ships a themeable Card tied to design-system tokens for radius, shadow, and surface color — plus a Liquid Glass variant that falls back gracefully on older iOS.

Get The Swift Kit — $99

Rather have it done for you? I set up and ship your app from $499. See how

How to build a reusable card component

Promote the card look into a ViewModifier so every card in the app stays consistent and updates in one place.

  1. 1

    Compose the base

    Pad content, add a rounded background, and a subtle shadow.

    content.padding().background(surface, in: RoundedRectangle(cornerRadius: 16))
  2. 2

    Wrap in a ViewModifier

    Move those modifiers into a CardStyle: ViewModifier body.

    struct CardStyle: ViewModifier { func body(content: Content) -> some View { /* ... */ } }
  3. 3

    Expose an extension

    Add a View extension so applying it reads as one modifier.

    extension View { func cardStyle() -> some View { modifier(CardStyle()) } }
  4. 4

    Make it tappable

    Wrap the styled card in a NavigationLink with .buttonStyle(.plain).

    NavigationLink(value: item) { CardBody().cardStyle() }.buttonStyle(.plain)

Frequently Asked Questions

Why does the image inside my SwiftUI card poke out past the rounded corners?
The rounded background clips its own fill, but an image that reaches the card edges needs the whole container clipped. Apply .clipShape(RoundedRectangle(cornerRadius:)) to the outer card after laying out content so overflowing image corners are masked to the same radius.
How do I make a whole SwiftUI card tappable for navigation without it turning blue?
Wrap the card in a NavigationLink and add .buttonStyle(.plain). The plain style stops the link from tinting your content blue and applying list-row chrome, while the entire card surface remains tappable. Make sure the card fills its frame (maxWidth: .infinity) so the tap target matches the visual.
What's the cleanest way to reuse the same SwiftUI card look across many screens?
Package padding, background, clip, and shadow into a ViewModifier called CardStyle, then expose it via a View extension so applying it is a single .cardStyle() call. Changing the radius or shadow in that one type then updates every card in the app.
How do I apply the iOS 26 Liquid Glass effect to a SwiftUI card and still support older iOS?
Use .glassEffect(in: yourShape) inside an if #available(iOS 26, *) branch, and fall back to .background(.ultraThinMaterial, in: shape) on iOS 16–25. Group adjacent glass cards in a GlassEffectContainer so their surfaces blend naturally.
Why does my SwiftUI card background not wrap the padding around the content?
Modifier order matters: apply .padding() before .background. If the background comes first it fills only the unpadded content, and the padding then sits outside the fill. Pad, then background, then clip and shadow.

Keep exploring

A design system, not a pile of modifiers

The Swift Kit is a $99 one-time SwiftUI boilerplate with a token-driven design system, reusable cards, auth, and paywalls — so your UI stays consistent from the first screen to the hundredth.

Get The Swift Kit — $99

One-time purchase · Lifetime updates · 14-day refund