KPI Card

Dashboard metric card with icon, title, value (numericText animation), tint color, and customizable trailing slot. Includes SWKPIDeltaTag for period-over-period comparison

Free
Free with a ShipSwift account.

Copies a short instruction with your private download link. Paste it into your agent: it downloads the whole skill (the source code, the setup steps and the mistakes we already made for you) and builds the screen.

Works with

+70

What it does

SWKPICard is a dashboard KPI tile designed for revenue dashboards, analytics summaries, and admin panels where multiple metrics are arranged in a 2×N grid. It has three layout layers:

  1. Header row -- SF Symbol icon (tinted) + title in caption.secondary.
  2. Hero value -- title2.bold in the tint color, animated via .contentTransition(.numericText()) so updating the value smoothly morphs the digits.
  3. Trailing slot -- a @ViewBuilder closure for delta tags, unit labels, date ranges, or any caption-level metadata. The caller has full control over what sits in this slot.

Card chrome: 14pt padding, 16pt corner radius systemBackground fill (with macOS windowBackgroundColor equivalent), a faint black 0.04 shadow, and a tint × 0.15 stroke. Designed to sit on a soft-colored page background (cream, ivory, gray.opacity(0.06)) rather than pure white.

Ships with a companion SWKPIDeltaTag view -- a stock period-over-period indicator (green up-arrow / red down-arrow + signed percentage + comparison label). It gracefully degrades to a "No data" placeholder when delta is nil, so card chrome stays consistent across populated and empty states.

On value formatting: the caller is responsible for formatting the value string. This keeps the component locale-agnostic and lets you control currency symbols, thousand separators, and unit suffixes ("$1.2K", "¥1,234", "42 cups", "98%", etc.).

A convenience initializer (extension where Trailing == EmptyView) lets you omit the trailing closure entirely when no metadata is needed.

What you get

  • ✓368 lines of source code, ready to paste
  • ✓Source Code
  • ✓Usage
componentdisplaykpidashboardmetriccardSwiftUI

Usage

// 1. Single card with SWKPIDeltaTag trailing (most common dashboard pattern)
SWKPICard(
    title: "Today's Revenue",
    value: "$1,234",
    icon: "dollarsign.circle.fill",
    tint: .brown
) {
    SWKPIDeltaTag(delta: 12.5)
}

// 2. No trailing slot (convenience initializer where Trailing == EmptyView)
SWKPICard(
    title: "Total Members",
    value: "1,024",
    icon: "person.3.fill",
    tint: .blue
)

// 3. Custom trailing -- unit label, date range, or any caption-level metadata
SWKPICard(
    title: "Cups Sold",
    value: "128",
    icon: "cup.and.saucer.fill",
    tint: .orange
) {
    Text("Unit: cups")
        .font(.caption2)
        .foregroundStyle(.secondary)
}

// 4. 2-column grid layout (typical dashboard)
LazyVGrid(
    columns: [
        GridItem(.flexible(), spacing: 12),
        GridItem(.flexible(), spacing: 12)
    ],
    spacing: 12
) {
    SWKPICard(title: "Revenue", value: "$1,234", icon: "dollarsign.circle.fill", tint: .brown) {
        SWKPIDeltaTag(delta: 12.5)
    }
    SWKPICard(title: "Orders", value: "42", icon: "bag.fill", tint: .blue) {
        SWKPIDeltaTag(delta: -3.2)
    }
    SWKPICard(title: "New Members", value: "8", icon: "person.2.fill", tint: .pink) {
        SWKPIDeltaTag(delta: nil)   // graceful "No data" fallback
    }
    SWKPICard(title: "Conversion", value: "4.2%", icon: "chart.line.uptrend.xyaxis", tint: .green) {
        SWKPIDeltaTag(delta: 0.8, comparisonLabel: "vs last week")
    }
}

// 5. Animated value updates (the .numericText() content transition is built-in)
struct DashboardView: View {
    @State private var revenue: Int = 1234
    private let timer = Timer.publish(every: 2, on: .main, in: .common).autoconnect()
    var body: some View {
        SWKPICard(
            title: "Revenue",
            value: "$\(revenue)",
            icon: "dollarsign.circle.fill",
            tint: .brown
        ) {
            SWKPIDeltaTag(delta: 12.5)
        }
        .onReceive(timer) { _ in
            withAnimation { revenue += Int.random(in: 1...100) }
            // digits smoothly morph via .contentTransition(.numericText())
        }
    }
}