Skip to content
Daniel S edited this page Jul 2, 2026 · 1 revision

Layout

This page covers DFCard, DesignFoundation's primary layout container. For navigation containers, see Navigation. For list components, see Lists-Tables-and-Data. For spacing tokens and surface colors referenced throughout this page, see Theming.


DFCard

DFCard is a themed, interactive-capable surface container. It wraps arbitrary View content inside a styled background, handles press animations, respects the active theme, and participates in the SwiftUI accessibility tree automatically.

import DesignFoundation

Initializer

public struct DFCard<Content: View>: View {
    public init(
        action: (() -> Void)? = nil,
        @ViewBuilder content: () -> Content
    )
}
Parameter Type Default Description
action (() -> Void)? nil Optional tap handler. When provided, the card becomes interactive: it gains .isButton accessibility trait and plays a press-scale animation on tap.
content @ViewBuilder () -> Content Any SwiftUI view hierarchy displayed inside the card surface.

When action is nil the card is a passive container — no tap handling, no press animation, and no .isButton accessibility trait.

When action is provided DFCard tracks a drag gesture internally to drive a 0.98 scale press effect. This animation is automatically suppressed when accessibilityReduceMotion is enabled.


Padding and Corner Radius

Padding and corner radius are owned by the active card style and resolved in this order:

  1. theme.components.card.padding — per-component token override
  2. theme.spacing.lg (16 pt) — fallback from the spacing scale

Corner radius resolves identically:

  1. theme.components.card.cornerRadius
  2. theme.radius.lg — fallback

Card Styles

Apply with .dfCardStyle(_:). The modifier cascades — set it on a parent container to style all descendant cards unless overridden closer to the leaf.

Style Static accessor Background Shadow Border Availability
Elevated .elevated theme.colors.surface theme.shadows.sm None iOS 18+, macOS 15+
Outlined .outlined theme.colors.surface None theme.colors.border 1 pt stroke iOS 18+, macOS 15+
Filled .filled theme.colors.surfaceElevated None None iOS 18+, macOS 15+
Glass .glass .regularMaterial None white.opacity(0.2) 0.5 pt iOS 26+, macOS 26+

Elevated is the default when no style is explicitly applied.

Outlined replaces the shadow with a 1-point theme.colors.border stroke. Use when the background is already elevated.

Filled uses theme.colors.surfaceElevated — useful for secondary panels or cards nested inside an already-elevated container.

Glass applies SwiftUI's .regularMaterial background and a translucent white hairline border. Requires iOS 26 or macOS 26.


Custom Card Style

Conform to DFCardStyle and annotate as Sendable:

public protocol DFCardStyle {
    associatedtype Body: View
    @ViewBuilder func makeBody(configuration: DFCardStyleConfiguration) -> Body
}

DFCardStyleConfiguration gives you everything you need:

Property Type Notes
content AnyView The card's child view hierarchy
isPressed Bool true while the user's finger is down
isDisabled Bool Mirrors @Environment(\.isEnabled)
isInteractive Bool true when the card has an action closure
theme DFTheme The active theme
struct DashedCardStyle: DFCardStyle, Sendable {
    func makeBody(configuration: DFCardStyleConfiguration) -> some View {
        let theme = configuration.theme
        configuration.content
            .padding(theme.spacing.lg)
            .background(theme.colors.surface)
            .clipShape(RoundedRectangle(cornerRadius: theme.radius.md))
            .overlay(
                RoundedRectangle(cornerRadius: theme.radius.md)
                    .stroke(theme.colors.border, style: StrokeStyle(lineWidth: 1.5, dash: [6]))
            )
            .opacity(configuration.isDisabled ? 0.5 : 1.0)
    }
}

DFCard { Text("Drop files here") }
    .dfCardStyle(DashedCardStyle())

Accessibility

DFCard wraps its content in .accessibilityElement(children: .contain), so all child elements remain individually focusable. When an action is provided, .isButton is added to the card's own accessibility traits.

Press animations automatically deactivate when the system accessibilityReduceMotion preference is on.


Examples

1. Simple info card

DFCard {
    VStack(alignment: .leading, spacing: theme.spacing.sm) {
        DFText("What is SwiftUI?", style: .headline)
        DFText(
            "SwiftUI is Apple's declarative UI framework for building apps across all Apple platforms.",
            style: .body
        )
    }
}

2. Profile card

DFCard(action: { navigateToProfile(user) }) {
    HStack(spacing: theme.spacing.md) {
        DFAvatar(url: user.avatarURL, size: 48)

        VStack(alignment: .leading, spacing: theme.spacing.xs) {
            DFText(user.displayName, style: .headline)
            DFText(user.role, style: .caption)
                .foregroundColor(theme.colors.textSecondary)
        }

        Spacer()

        DFIcon("chevron.right", color: theme.colors.textSecondary)
    }
}
.accessibilityLabel("\(user.displayName), \(user.role)")

3. Settings section

DFCard {
    VStack(alignment: .leading, spacing: 0) {
        DFText("Notifications", style: .subheadline)
            .foregroundColor(theme.colors.textSecondary)
            .padding(.bottom, theme.spacing.sm)

        DFToggle("Push notifications", isOn: $pushEnabled)
        DFDivider()
        DFToggle("Email digest", isOn: $emailEnabled)
        DFDivider()
        DFToggle("In-app sounds", isOn: $soundsEnabled)
    }
}
.dfCardStyle(.outlined)

4. Metric card

@Environment(\.dfTheme) private var theme

DFCard {
    VStack(alignment: .leading, spacing: theme.spacing.sm) {
        HStack(alignment: .top) {
            DFText("Monthly Revenue", style: .caption)
                .foregroundColor(theme.colors.textSecondary)
            Spacer()
            DFBadge(text: "+12%", color: theme.colors.success)
        }

        DFText("$48,200", style: .largeTitle)
            .foregroundColor(theme.colors.textPrimary)

        HStack(spacing: theme.spacing.xs) {
            DFIcon("arrow.up.right", size: .sm, color: theme.colors.success)
            DFText("vs last month", style: .caption)
                .foregroundColor(theme.colors.textSecondary)
        }
    }
}

5. Nested cards — elevated inner panel inside a filled outer card

DFCard {
    VStack(alignment: .leading, spacing: theme.spacing.md) {
        DFText("Recent Deployments", style: .headline)

        ForEach(deployments) { deployment in
            DFCard(action: { openDeployment(deployment) }) {
                HStack(spacing: theme.spacing.md) {
                    DFIcon(
                        deployment.status == .success ? "checkmark.seal.fill" : "xmark.seal.fill",
                        color: deployment.status == .success
                            ? theme.colors.success
                            : theme.colors.error
                    )
                    VStack(alignment: .leading, spacing: theme.spacing.xs) {
                        DFText(deployment.commitMessage, style: .subheadline)
                        DFText(deployment.relativeDate, style: .caption)
                            .foregroundColor(theme.colors.textSecondary)
                    }
                    Spacer()
                    DFBadge(
                        text: deployment.status.rawValue,
                        color: deployment.status == .success
                            ? theme.colors.success
                            : theme.colors.error
                    )
                }
            }
            .dfCardStyle(.elevated)
        }
    }
}
.dfCardStyle(.filled)

6. Using DFCard as a List Item Container

ScrollView {
    LazyVStack(spacing: theme.spacing.sm) {
        ForEach(items) { item in
            DFCard(action: { select(item) }) {
                DFListRow(item.title, subtitle: item.subtitle, icon: item.icon)
            }
        }
    }
    .padding(theme.spacing.lg)
}

Combining DFCard with DFSkeleton

DFCard {
    VStack(alignment: .leading, spacing: theme.spacing.sm) {
        DFSkeleton(width: 40, height: 40, shape: .circle)
        DFSkeleton(width: 160, height: 14)
        DFSkeleton(width: 220, height: 12)
        DFSkeleton(width: 190, height: 12)
    }
}

Swap the skeleton content for real data once your async load completes. Keep the card in the hierarchy to prevent layout jumps.


Related Pages

  • Theming — surface colors, spacing scale, radius tokens, and theme presets
  • Lists-Tables-and-DataDFList, DFListRow, DFTable, DFDataGrid
  • PrimitivesDFText, DFBadge, DFAvatar, DFIcon, DFDivider
  • Getting-Started — installation, theme setup, and first component

If you need production-ready dashboard stat cards, metric grids, activity feed cards, or full content layouts, DesignFoundation Pro ships these ready to drop in. See https://nerdsnipe-inc.github.io/design-foundation/pro/ for details.

Clone this wiki locally