Repository navigation
Popups
.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.
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")
}
}
}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) }
}
}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.
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 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.
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")
}
}
}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)
}
}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 |
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") }
)
}
}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)
}
}.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
}
}
}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.
.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") }
}
}
}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.
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+.