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

Icons

DFIcon is DesignFoundation's unified icon primitive. It wraps SF Symbols and custom images behind a single, consistent API — enforcing theme-aware sizing, automatic rendering modes, and zero-config accessibility defaults.

Related pages: Primitives · Theming · Buttons · Text-Fields-and-Inputs · Lists-Tables-and-Data


Table of Contents

  1. Overview
  2. Initializer Reference
  3. DFIconSize Reference
  4. Style Variants
  5. Color Guide
  6. SF Symbol Weights and Rendering Modes
  7. Custom Image Icons
  8. Accessibility
  9. Complete Examples
  10. Combining DFIcon with Other Primitives

Overview

Why DFIcon instead of Image(systemName:)?

Concern Image(systemName:) DFIcon
Sizing Manual .font(.system(size: 20)) per call-site Semantic size tokens (xsxl) from the theme
Color Manual .foregroundStyle(…) Inherits environment or one explicit param
Rendering mode Default symbol rendering .hierarchical on iOS 18+ / macOS 15+ automatically
Accessibility Announced as "image" to VoiceOver Decorative by default (hidden); opt-in label when semantic
Custom images Image("logo") — different API DFIcon(image: Image("logo")) — same API
// Before
Image(systemName: "star.fill")
    .foregroundStyle(theme.colors.primary)
    .font(.system(size: 20))
    .accessibilityHidden(true)

// After
DFIcon("star.fill", size: .md, color: theme.colors.primary)

Initializer Reference

SF Symbol

DFIcon("house.fill")                                    // md size, inherits foreground
DFIcon("house.fill", size: .lg)
DFIcon("house.fill", size: .lg, color: theme.colors.primary)

Custom Image

DFIcon(image: Image("AppLogo"), size: .xl)
DFIcon(image: Image("BrandGlyph"), size: .md, color: theme.colors.primary)  // template mode

DFIconSize Reference

public enum DFIconSize: Sendable {
    case xs   // 12 pt
    case sm   // 16 pt
    case md   // 20 pt  ← default
    case lg   // 28 pt
    case xl   // 36 pt
}
Case Point size Recommended contexts
.xs 12 pt Inline with .caption text, tight table cells
.sm 16 pt Leading icons in DFTextField, DFListRow icons
.md 20 pt Default — toolbar buttons, row actions
.lg 28 pt Section headers, card header icons
.xl 36 pt Empty state heroes, feature highlights
HStack(spacing: theme.spacing.md) {
    DFIcon("star.fill", size: .xs)
    DFIcon("star.fill", size: .sm)
    DFIcon("star.fill", size: .md)
    DFIcon("star.fill", size: .lg)
    DFIcon("star.fill", size: .xl)
}

Style Variants

.standard (default)

Renders the symbol at its natural weight. Use in nav bars, list rows, alongside text.

DFIcon("bell")   // standard, implicit

.circle

Icon centered inside a filled circle container. Best for empty states and feature highlights.

DFIcon("star.fill", size: .xl).dfIconStyle(.circle)
DFIcon("checkmark", size: .md, color: theme.colors.success).dfIconStyle(.circle)

// Empty state pattern
VStack(spacing: theme.spacing.lg) {
    DFIcon("tray", size: .xl, color: theme.colors.textSecondary)
        .dfIconStyle(.circle)
    DFText("No messages", style: .headline)
}

.filled

Automatically appends .fill to the symbol name if not already present.

DFIcon("heart").dfIconStyle(.filled)     // renders heart.fill
DFIcon("star").dfIconStyle(.filled)      // renders star.fill

// Toggle filled/unfilled based on state
DFIcon("heart", size: .md, color: isLiked ? theme.colors.error : theme.colors.textSecondary)
    .dfIconStyle(isLiked ? .filled : .standard)

Color Guide

// Default: inherits foreground
DFIcon("house.fill")

// Brand accent
DFIcon("sparkles", size: .md, color: theme.colors.primary)

// Muted / decorative
DFIcon("magnifyingglass", size: .sm, color: theme.colors.textSecondary)

// Semantic colors — prefer these over raw Color for status
DFIcon("checkmark.circle.fill", size: .sm, color: theme.colors.success)
DFIcon("exclamationmark.triangle.fill", size: .sm, color: theme.colors.warning)
DFIcon("xmark.circle.fill", size: .sm, color: theme.colors.error)

Decision guide:

  • Status (success / warning / error)? → theme.colors.success / .warning / .error
  • Primary action or active selection? → theme.colors.primary
  • Decorative or supplementary? → theme.colors.textSecondary
  • Match surrounding text? → omit color, inherit foreground

SF Symbol Weights and Rendering Modes

DFIcon uses .hierarchical rendering mode on iOS 18+ / macOS 15+ automatically. Override when needed:

// Monochrome (override hierarchical)
DFIcon("cloud.sun.rain.fill", size: .lg, color: theme.colors.primary)
    .symbolRenderingMode(.monochrome)

// Palette — two explicit colors
DFIcon("person.badge.plus", size: .lg)
    .symbolRenderingMode(.palette)
    .foregroundStyle(theme.colors.primary, theme.colors.textSecondary)

// Bold weight for emphasis
DFIcon("bolt.fill", size: .md, color: theme.colors.warning)
    .fontWeight(.bold)

Custom Image Icons

// Asset catalog image
DFIcon(image: Image("BrandMark"), size: .lg)

// From UIImage
DFIcon(image: Image(uiImage: downloadedImage), size: .md)

Mark the asset as Template Image in Xcode to allow tinting:

// Template image: color param takes effect
DFIcon(image: Image("CheckIcon"), size: .sm, color: theme.colors.success)

Accessibility

DFIcon is hidden from VoiceOver by default — correct for decorative icons adjacent to text.

// Standalone semantic icon — needs a label
Button { deleteItem() } label: {
    DFIcon("trash", size: .md, color: theme.colors.error)
        .accessibilityLabel("Delete item")
        .accessibilityHidden(false)
}

// Status icon with no adjacent text
DFIcon("checkmark.circle.fill", size: .sm, color: theme.colors.success)
    .accessibilityLabel("Delivered")
    .accessibilityHidden(false)

Checklist:

  • Icon alongside visible DFText? → leave decorative (default)
  • Icon is the only content in a tappable element? → .accessibilityLabel + .accessibilityHidden(false)
  • Status icon with no label in a table cell? → .accessibilityLabel + .accessibilityHidden(false)

Complete Examples

1. Navigation Tab Bar Icons

private let tabItems: [DFTabItem] = [
    DFTabItem(id: "home",     icon: "house.fill",    label: "Home"),
    DFTabItem(id: "explore",  icon: "safari.fill",   label: "Explore"),
    DFTabItem(id: "messages", icon: "message.fill",  label: "Messages"),
    DFTabItem(id: "alerts",   icon: "bell.fill",     label: "Alerts"),
    DFTabItem(id: "profile",  icon: "person.fill",   label: "Profile"),
]

2. Field Leading Icons

DFTextField("Email address", text: $email, leadingIcon: "envelope")
DFTextField("Search", text: $query, leadingIcon: "magnifyingglass")
DFSecureField("Password", text: $password)  // lock icon built in

3. Status Icons in List Rows

func statusIcon(_ status: OrderStatus) -> some View {
    switch status {
    case .delivered:
        DFIcon("checkmark.circle.fill", size: .sm, color: theme.colors.success)
            .accessibilityLabel("Delivered").accessibilityHidden(false)
    case .failed:
        DFIcon("xmark.circle.fill", size: .sm, color: theme.colors.error)
            .accessibilityLabel("Failed").accessibilityHidden(false)
    case .pending:
        DFIcon("clock.fill", size: .sm, color: theme.colors.warning)
            .accessibilityLabel("Pending").accessibilityHidden(false)
    }
}

4. Empty State Hero Icon

VStack(spacing: theme.spacing.xl) {
    DFIcon("tray", size: .xl, color: theme.colors.textSecondary)
        .dfIconStyle(.circle)

    VStack(spacing: theme.spacing.sm) {
        DFText("Nothing here yet", style: .headline)
        DFText("Messages you receive will appear here.", style: .caption)
            .multilineTextAlignment(.center)
    }

    DFButton("Compose", icon: "square.and.pencil") { onCompose() }
}
.padding(theme.spacing.xxl)
.frame(maxWidth: 320)

5. Section Header with Icon and Title

struct SectionHeader: View {
    let systemName: String
    let title: String
    @Environment(\.dfTheme) private var theme

    var body: some View {
        HStack(spacing: theme.spacing.sm) {
            DFIcon(systemName, size: .lg, color: theme.colors.primary)
            DFText(title, style: .headline)
            Spacer()
        }
        .padding(.bottom, theme.spacing.xs)
    }
}

6. Icon Grid for Feature Comparison

struct FeatureRow: View {
    let feature: String
    let hasBasic: Bool
    let hasPro: Bool
    @Environment(\.dfTheme) private var theme

    var body: some View {
        HStack {
            DFText(feature, style: .body)
            Spacer()
            tierIcon(included: hasBasic).frame(width: 60)
            tierIcon(included: hasPro).frame(width: 60)
        }
        .padding(.vertical, theme.spacing.xs)
    }

    @ViewBuilder
    private func tierIcon(included: Bool) -> some View {
        Group {
            if included {
                DFIcon("checkmark.circle.fill", size: .sm, color: theme.colors.success)
            } else {
                DFIcon("minus.circle", size: .sm, color: theme.colors.border)
            }
        }
        .frame(maxWidth: .infinity, alignment: .center)
    }
}

7. Custom Logo Image as DFIcon

struct AppHeader: View {
    @Environment(\.dfTheme) private var theme

    var body: some View {
        HStack(spacing: theme.spacing.sm) {
            DFIcon(image: Image("AppWordmark"), size: .lg)  // original rendering
            DFDivider().frame(height: 24)
            DFText("Dashboard", style: .headline)
            Spacer()
            DFIcon(image: Image("BrandGlyph"), size: .md, color: theme.colors.primary)  // template
        }
        .padding(.horizontal, theme.spacing.lg)
        .background(theme.colors.surface)
    }
}

8. Animated Icon with SF Symbol Effect

// Bounce on tap (iOS 17+ / macOS 14+)
Button { isFavorited.toggle() } label: {
    DFIcon(
        isFavorited ? "heart.fill" : "heart",
        size: .lg,
        color: isFavorited ? theme.colors.error : theme.colors.textSecondary
    )
    .symbolEffect(.bounce, value: isFavorited)
    .accessibilityLabel(isFavorited ? "Remove from favorites" : "Add to favorites")
    .accessibilityHidden(false)
}
.buttonStyle(.plain)

// Pulse for live state
DFIcon("antenna.radiowaves.left.and.right", size: .md, color: theme.colors.success)
    .symbolEffect(.pulse)

// Replace animation on symbol name change
DFIcon(isFavorited ? "star.fill" : "star", size: .md, color: theme.colors.primary)
    .contentTransition(.symbolEffect(.replace))

Combining DFIcon with Other Primitives

DFIcon + DFText in a Labeled Row

struct LabeledIconRow: View {
    let systemName: String
    let label: String
    let value: String
    @Environment(\.dfTheme) private var theme

    var body: some View {
        HStack(spacing: theme.spacing.sm) {
            DFIcon(systemName, size: .sm, color: theme.colors.textSecondary)
            DFText(label, style: .body)
            Spacer()
            DFText(value, style: .body).foregroundStyle(theme.colors.textSecondary)
        }
    }
}

DFIcon + DFBadge for Notification Indicators

struct NotificationIcon: View {
    let count: Int
    @Environment(\.dfTheme) private var theme

    var body: some View {
        ZStack(alignment: .topTrailing) {
            DFIcon("bell.fill", size: .md, color: theme.colors.textPrimary)
            if count > 0 {
                DFBadge(text: count > 99 ? "99+" : "\(count)", color: .red)
                    .offset(x: 8, y: -6)
            }
        }
    }
}

DFIcon in a Feature Highlight Row

HStack(spacing: theme.spacing.md) {
    DFIcon("sparkles", size: .lg, color: theme.colors.primary)
        .dfIconStyle(.circle)

    VStack(alignment: .leading, spacing: theme.spacing.xs) {
        HStack(spacing: theme.spacing.sm) {
            DFText("AI Writing Assistant", style: .headline)
            DFBadge(text: "New", color: .purple)
        }
        DFText("Draft emails and messages in seconds.", style: .caption)
    }

    Spacer()
    DFIcon("chevron.right", size: .sm, color: theme.colors.textSecondary)
}
.padding(theme.spacing.md)

Quick Reference

DFIcon("star.fill", size: .xs)    // 12 pt
DFIcon("star.fill", size: .sm)    // 16 pt
DFIcon("star.fill", size: .md)    // 20 pt (default)
DFIcon("star.fill", size: .lg)    // 28 pt
DFIcon("star.fill", size: .xl)    // 36 pt

DFIcon("star.fill").dfIconStyle(.standard)  // default
DFIcon("star.fill").dfIconStyle(.circle)    // circular container
DFIcon("star").dfIconStyle(.filled)         // auto → star.fill

DFIcon("star.fill")                                    // inherits foreground
DFIcon("star.fill", color: theme.colors.primary)       // brand accent
DFIcon("star.fill", color: theme.colors.textSecondary) // muted
DFIcon("star.fill", color: theme.colors.success)       // semantic green

// Accessibility
DFIcon("trash")                             // decorative (default)
DFIcon("trash")
    .accessibilityLabel("Delete")
    .accessibilityHidden(false)             // semantic

See also: Primitives · Buttons · Text-Fields-and-Inputs · Lists-Tables-and-Data · Navigation · Theming

Clone this wiki locally