Skip to content
Daniel S edited this page Sep 21, 2026 · 3 revisions

Popups and Toasts

.dfPopup is the single presentation engine behind DesignFoundation's toasts, floaters, centered cards and bottom sheets. It is a view modifier, not a constructible view: you attach it to the content the popup should appear over and drive it with a binding. DFToastQueue and .dfToast() run on the same engine.

Popups are themed automatically: corner radius, padding, max width and backdrop opacity come from the active DFTheme, and the surface look comes from a DFPopupStyle.

See it in motion: the docs site has real Simulator recordings of every popup kind, style, transition and toast style: Popups on the docs site. The Pro popups are at Pro popups.

Quick Start

struct ContentView: View {
    @State private var showCard = false
    @State private var showBanner = false

    var body: some View {
        VStack {
            DFButton("Show card") { showCard = true }
            DFButton("Show banner") { showBanner = true }
        }
        // Centered card with a dimmed backdrop (default configuration)
        .dfPopup(isPresented: $showCard) {
            Text("Hello from a popup")
        }
        // Full-width banner flush with the bottom edge
        .dfPopup(isPresented: $showBanner, configuration: .toast(position: .bottom)) {
            Text("Saved")
        }
    }
}

Presenting

Two modifiers are available. Both also take onDismiss:.

struct PresentingExample: View {
    struct Item: Identifiable { let id = 1; let title = "Item" }
    @State private var show = false
    @State private var selected: Item?

    var body: some View {
        Color.clear
            // Boolean-driven
            .dfPopup(isPresented: $show, configuration: .centered, onDismiss: { }) { Text("Content") }
            // Item-driven: the item must be Identifiable. Changing the item's id while
            // presented restarts auto-dismiss.
            .dfPopup(item: $selected) { item in Text(item.title) }
    }
}

Kinds

DFPopupKind controls the shape of the popup surface.

Kind Shape Preset
.center Width-limited card, inset from the edges, dimmed backdrop DFPopupConfiguration.centered
.toast Full-width and flush with the edge, extending under the safe area, no backdrop .toast(position:autoDismissAfter:dismissOnDrag:)
.floater Inset from the edges by the popup padding token, rounded corners, no backdrop .floater(position:autoDismissAfter:dismissOnDrag:)
.sheet Bottom-anchored, full width, rounded top corners, grabber, drag down to dismiss .sheet(dismissOnDrag:backdrop:dismissOnOutsideTap:)

Toast and floater presets let touches pass through to the host view. A sheet always rests at the bottom whatever its position says, and needs a longer pull to dismiss than the other kinds.

Positions

DFPopupPosition has nine values: .topLeading, .top, .topTrailing, .leading, .center, .trailing, .bottomLeading, .bottom, .bottomTrailing. Each position has an exitEdge, the screen edge the popup enters from and leaves toward (.center uses the bottom).

struct FloaterExample: View {
    @State private var show = false

    var body: some View {
        Color.clear
            .dfPopup(isPresented: $show, configuration: .floater(position: .bottomTrailing)) {
                Text("Inset, drag to dismiss")
            }
    }
}

DFPopupConfiguration

DFPopupConfiguration is a plain, Sendable, Equatable value, safe to store and share.

Property Default Meaning
kind .center Surface shape (see above)
position .center Where the popup rests
transition .automatic .automatic (scale for center, slide otherwise), .slide, .scale, .fade, .none, .asymmetric(insert:remove:)
animation nil Overrides the theme animation; nil uses the theme
autoDismissAfter nil Seconds until the popup dismisses itself
dismissOnTap false Tap the popup to dismiss
dismissOnOutsideTap true Tap outside to dismiss
dismissOnDrag false Drag toward the exit edge to dismiss (never dismisses when dragging away from it)
dimsBackground true Dim the host and block touches to it
backdrop nil DFPopupBackdrop (.none, .dim, .blur). A non-nil value wins over dimsBackground; nil derives it (true gives .dim, false gives .none). resolvedBackdrop reports the result

Reduce Motion swaps slide and scale transitions for a short fade. On macOS, Escape dismisses the popup.

Backdrops and the sheet kind

struct SheetExample: View {
    @State private var showSheet = false
    @State private var showBlurred = false

    var body: some View {
        Color.clear
            .dfPopup(isPresented: $showSheet, configuration: .sheet(backdrop: .blur)) {
                Text("Bottom sheet")
            }
            .dfPopup(isPresented: $showBlurred, configuration: DFPopupConfiguration(backdrop: .blur)) {
                Text("Blurred backdrop")
            }
    }
}

Surface styles

Restyle every popup in a subtree with .dfPopupStyle(_:).

Style Look
.standard (default) Surface fill, hairline border, layered soft shadow, continuous corners
.frosted Material blur
.glass Liquid Glass (iOS/macOS 26+); honors theme.materials.preferLiquidGlass
.accent Filled with the primary color
.gradient Primary to accent gradient with a soft glow
.inverse textPrimary as the background
.outlined 1.5 pt border
.tinted(_:) Severity wash for a DFToastSeverity (.info, .success, .warning, .error)

Colored styles (.accent, .gradient, filled toasts) resolve a foreground that reaches WCAG AA (4.5:1) on every fill stop and re-point the subtree's theme and default DFButton at it, so plain content stays legible in every preset, light and dark.

struct StyledPopup: View {
    @State private var show = false

    var body: some View {
        Color.clear
            .dfPopup(isPresented: $show) {
                DFPopupCard(title: "Themed", message: "Style and theme sit outside the popup.")
            }
            .dfPopupStyle(.frosted)
            .dfThemePreset(.slate)
    }
}

Modifier order matters

Popups and toasts are drawn in an overlay owned by .dfPopup and .dfToast, so they read the environment from outside that modifier. Apply .dfPopupStyle, .dfToastStyle, .dfTheme and .dfThemePreset at or after (outside) the presenting modifier:

Order Result
.dfPopup(...).dfPopupStyle(.frosted).dfThemePreset(.slate) Works
.dfThemePreset(.slate).dfPopup(...) The popup is drawn in the default theme
.dfPopupStyle(.frosted).dfPopup(...) The style is ignored

DFPopupCard

Ready-made popup content: an icon badge or edge-to-edge hero media, a title, a message, up to three actions (primary filled, secondary translucent, tertiary plain text) and an optional close button. Alignment is .center or .leading. The pieces are also public: DFPopupHeader, DFPopupActions, DFPopupIconBadge, DFPopupAction.

struct CardExample: View {
    @State private var showPopup = false

    var body: some View {
        Color.clear.dfPopup(isPresented: $showPopup) {
            DFPopupCard(
                icon: "sparkles",
                title: "Upgrade to Pro",
                message: "Unlock unlimited projects.",
                primaryAction: DFPopupAction("Upgrade") { },
                secondaryAction: DFPopupAction("Later") { },
                tertiaryAction: DFPopupAction("Delete", role: .destructive) { },
                onClose: { showPopup = false }
            )
        }
    }
}

// Hero media bleeds to the surface edges; content sits between the message and the actions.
struct HeroCard: View {
    var body: some View {
        DFPopupCard(
            title: "Summer sale",
            primaryAction: DFPopupAction("Shop") { },
            media: { Color.orange.frame(height: 120) },
            content: { Text("40% off") }
        )
    }
}

Toasts

DFToastQueue.shared.show(text:icon:duration:severity:position:title:actionTitle:action:) queues a toast; apply .dfToast() once at the root. Only text is required. Toasts can appear at any of the nine positions (default .top), and support tap-to-dismiss and swipe-to-dismiss toward their edge. Tapping the action runs it, then dismisses the toast. New toasts are announced to VoiceOver. See Alerts-and-Toasts for queue behavior and severities.

struct ToastExample: View {
    var body: some View {
        DFButton("Delete") {
            DFToastQueue.shared.show(text: "Saved", severity: .success, position: .bottom)
            DFToastQueue.shared.show(
                text: "Moved to the trash", icon: "trash", severity: .error,
                title: "Deleted", actionTitle: "Undo", action: { }
            )
        }
        .dfToast(style: .tinted)
    }
}

Toast styles

.default, .tinted, .filled, .inverse, .frosted, .glass (iOS/macOS 26+), .banner (full-width, flush to the edge, severity stripe) and .compact. A style's layout (DFToastLayout: .floating by default, .flush for .banner) decides whether the toast is drawn as a floater or a flush toast.

Style the toast layer with .dfToast(style:). The same ordering rule as popups applies:

struct ToastOrderExample: View {
    var body: some View {
        VStack {
            Text("A").dfToast(style: .tinted)                 // works
            Text("B").dfToast().dfToastStyle(.compact)        // works
            // Text("C").dfToastStyle(.tinted).dfToast()      // does NOT restyle toasts
        }
    }
}

Tuning tokens

Tune a single theme's popups without touching the rest of the theme via DFPopupTokens:

var theme = DFTheme.slateLight
theme.components.popup = DFPopupTokens(cornerRadius: 20, maxWidth: 360, backdropOpacity: 0.5)

All fields are optional; nil inherits (cornerRadius from radius.lg, padding from spacing.lg, maxWidth 420, backdropOpacity 0.35). Popup entrances use the spring token DFAnimationTokens.spring.

DFPopupHost

.dfPopup is built on the public DFPopupHost. Use it directly only when you need to embed a popup layer inside a custom container.

struct HostExample: View {
    @State private var show = false

    var body: some View {
        Color.clear.overlay {
            DFPopupHost(isPresented: $show, configuration: .toast()) { Text("Hello") }
        }
    }
}

Advanced Popups

Scrollable popups with detents, window-level presentation above sheets, a priority queue, motion presets, haptics and a set of finished rich popups (celebration, permission, promo, rating, input, consent, action, undo and progress toasts, notification banners, live capsule, coachmark tours) are available in DesignFoundation Pro. See Advanced-Popups.

Availability

The popup engine arrived in DesignFoundation 1.6.0. Popup surface styles, DFPopupKind.sheet, DFPopupBackdrop, DFPopupCard, toast styles, toast titles and actions, and .dfToast(style:) require DesignFoundation 1.7.0 or later. No platform guards are needed on any iOS 18, macOS 15, or visionOS 2 target; .glass styles only take effect on iOS/macOS 26+.

Clone this wiki locally