-
Notifications
You must be signed in to change notification settings - Fork 0
Overlays and Feedback
DesignFoundation ships six overlay and feedback components: DFModal, DFSheet, DFPopover, DFTooltip, DFAlert, and DFToast. Each one is platform-adaptive, theme-aware, and follows the same style protocol pattern as the rest of the library.
Related pages: Getting-Started · Theming · Buttons · Forms-and-Validation
DFModal presents a focused full-screen or center-stage overlay that requires explicit dismissal. Use for multi-step flows, detail editing, or any interaction that should block the underlying content.
// 1. Boolean binding
func dfModal(isPresented: Binding<Bool>, @ViewBuilder content: () -> Content) -> some View
// 2. Optional item binding (dismisses when item becomes nil)
func dfModal<Item: Identifiable>(
item: Binding<Item?>,
@ViewBuilder content: (Item) -> Content
) -> some View| Style | Token | Description |
|---|---|---|
DFDefaultModalStyle |
.default |
Centered card on macOS; full sheet on iOS. |
DFFullScreenModalStyle |
.fullScreen |
Fills the entire screen on all platforms. |
DFGlassModalStyle |
.glass |
.regularMaterial backing. iOS 26+ / macOS 26+. |
Apply with .dfModalStyle(_:).
By default DFModal dismisses when the user taps the scrim. Disable scrim-tap dismissal:
YourView()
.dfModal(isPresented: $showModal) {
ConfirmationModal()
}
.dfModalDismissesOnScrimTap(false)DFButton("Add Project") { showProjectForm = true }
.dfModal(isPresented: $showProjectForm) {
ProjectFormView(onSave: { project in
save(project)
showProjectForm = false
})
}DFList(items) { item in
DFListRow(item.title)
.onTapGesture { selectedItem = item }
}
.dfModal(item: $selectedItem) { item in
ItemDetailModal(item: item, onDismiss: { selectedItem = nil })
}DFButton("Edit Profile") { showEditor = true }
.dfModal(isPresented: $showEditor) {
ProfileEditorView(onComplete: { showEditor = false })
.dfNavigationBar(title: "Edit Profile", trailing: {
DFButton("Save") { saveProfile(); showEditor = false }
})
}
.dfModalStyle(.fullScreen)DFButton("Preview") { showPreview = true }
.dfModal(isPresented: $showPreview) {
PreviewContent()
}
.dfModalStyle(.glass)DFButton("Start Import", role: .destructive) { showImport = true }
.dfModal(isPresented: $showImport) {
ImportProgressView(onComplete: { showImport = false })
}
.dfModalDismissesOnScrimTap(false)DFSheet presents content from the bottom edge of the screen on iOS and as a popover on macOS. Ideal for contextual controls, quick editing, and supplementary content that doesn't need full-screen prominence.
func dfSheet(isPresented: Binding<Bool>, @ViewBuilder content: () -> Content) -> some View| Detent | Token | Description |
|---|---|---|
.medium |
default on iOS | Sheet rests at 50% of screen height. Draggable to .large. |
.large |
.large |
Sheet covers most of the screen. |
.fraction(CGFloat) |
.fraction(_:) |
Custom fraction of screen height. |
Apply detent with .dfSheetDetent(_:).
DFListRow("Archive")
.onTapGesture { showArchiveOptions = true }
.dfSheet(isPresented: $showArchiveOptions) {
VStack(spacing: 16) {
DFText("Archive Options", style: .headline)
DFButton("Archive immediately") { archiveNow() }
DFButton("Schedule for later") { showScheduler = true }
DFButton("Cancel", role: .cancel) { showArchiveOptions = false }
.dfButtonStyle(.outlined)
}
.padding()
}.dfSheet(isPresented: $showProfileSheet) {
ProfileQuickViewSheet(user: user, onFollow: { follow(user) })
}
.dfSheetDetent(.medium).dfSheet(isPresented: $showDetailSheet) {
ItemDetailSheet(item: item)
}
.dfSheetDetent(.large).dfSheet(isPresented: $showFilterPanel) {
FilterPanelView(filters: $filters)
}
.dfSheetDetent(.fraction(0.4))DFPopover shows a floating panel anchored to a specific view. On iOS it appears as a popover bubble; on macOS it renders as a proper popover with an arrow pointing to the anchor element.
func dfPopover<Content: View>(
isPresented: Binding<Bool>,
arrowEdge: Edge = .top,
@ViewBuilder content: () -> Content
) -> some View| Style | Token | Description |
|---|---|---|
DFDefaultPopoverStyle |
.default |
Themed card with shadow. |
DFTintedPopoverStyle |
.tinted |
Lightly color-tinted background. |
DFGlassPopoverStyle |
.glass |
Material backing. iOS 26+ / macOS 26+. |
Apply with .dfPopoverStyle(_:).
DFButton("?") { showInfo.toggle() }
.dfButtonStyle(.ghost)
.dfPopover(isPresented: $showInfo, arrowEdge: .bottom) {
VStack(alignment: .leading, spacing: 8) {
DFText("Why is this required?", style: .headline)
DFText(
"We use your billing address to calculate applicable taxes.",
style: .body
)
.frame(maxWidth: 260)
}
.padding()
}DFButton(action: { showColorPicker.toggle() }) {
Circle().fill(selectedColor).frame(width: 20, height: 20)
}
.dfButtonStyle(.ghost)
.dfPopover(isPresented: $showColorPicker, arrowEdge: .top) {
ColorPickerGrid(selection: $selectedColor)
.frame(width: 200, height: 200)
.padding()
}DFButton("Filters") { showFilters.toggle() }
.dfButtonStyle(.outlined)
.dfPopover(isPresented: $showFilters, arrowEdge: .top) {
FilterOptionsView(filters: $activeFilters) {
showFilters = false
}
.frame(width: 280)
}
.dfPopoverStyle(.glass)DFIcon("ellipsis")
.onTapGesture { showContextMenu.toggle() }
.dfPopover(isPresented: $showContextMenu, arrowEdge: .trailing) {
VStack(alignment: .leading, spacing: 4) {
DFButton("Edit") { edit(); showContextMenu = false }.dfButtonStyle(.ghost)
DFButton("Duplicate") { dupe(); showContextMenu = false }.dfButtonStyle(.ghost)
DFDivider()
DFButton("Delete", role: .destructive) {
delete()
showContextMenu = false
}.dfButtonStyle(.ghost)
}
.padding(.horizontal, 8)
.padding(.vertical, 4)
}DFTooltip is platform-adaptive: on macOS it renders as a native .help() modifier; on iOS it intercepts long-press gestures to show a floating label — no #if os() guard required.
public struct DFTooltip<Content: View>: View {
public init(
_ text: String,
placement: DFTooltipPlacement = .automatic,
@ViewBuilder content: () -> Content
)
}public enum DFTooltipPlacement: Sendable {
case automatic, top, bottom, leading, trailing
}| Style | Token | Description |
|---|---|---|
.default |
default | Solid dark (or light) rounded rectangle label. |
.glass |
.glass |
Material-backed tooltip. iOS 26+ / macOS 26+. |
Apply with .dfTooltipStyle(_:).
// Basic
DFTooltip("Export to CSV") {
DFIcon("square.and.arrow.up")
}
// Placement override
DFTooltip("Opens in browser", placement: .bottom) {
DFButton("Open Link") { openLink() }
.dfButtonStyle(.ghost)
}
// Tooltip on a disabled button
DFTooltip("You don't have permission to publish") {
DFButton("Publish") { publish() }
.disabled(!canPublish)
}DFAlert drives modal alert dialogs through a view modifier. All actions and their roles are defined declaratively — no imperative UIAlertController required.
public struct DFAlert: Sendable {
public let title: String
public let message: String?
public let actions: [DFAlertAction]
}
public struct DFAlertAction: Sendable {
public let title: String
public let role: DFAlertActionRole?
public let action: @Sendable () -> Void
}
public enum DFAlertActionRole: Sendable {
case cancel
case destructive
}// 1. Boolean binding
func dfAlert(isPresented: Binding<Bool>, alert: DFAlert) -> some View
// 2. Optional item binding
func dfAlert<Item: Identifiable & Sendable>(
item: Binding<Item?>,
alert: (Item) -> DFAlert
) -> some ViewDFButton("Log Out", role: .destructive) { showLogOut = true }
.dfAlert(
isPresented: $showLogOut,
alert: DFAlert(
title: "Log out?",
message: "You'll need to sign in again.",
actions: [
DFAlertAction(title: "Cancel", role: .cancel) { },
DFAlertAction(title: "Log Out", role: .destructive) { logOut() },
]
)
)DFList(projects) { project in
DFListRow(project.name)
.onTapGesture { projectToDelete = project }
}
.dfAlert(item: $projectToDelete) { project in
DFAlert(
title: "Delete \"\(project.name)\"?",
message: "All data will be permanently removed.",
actions: [
DFAlertAction(title: "Cancel", role: .cancel) { },
DFAlertAction(title: "Delete", role: .destructive) { delete(project) },
]
)
}.dfAlert(
isPresented: $showSaveOptions,
alert: DFAlert(
title: "Unsaved Changes",
message: "What would you like to do?",
actions: [
DFAlertAction(title: "Save & Exit") { saveAndExit() },
DFAlertAction(title: "Discard", role: .destructive) { discardAndExit() },
DFAlertAction(title: "Keep Editing", role: .cancel) { },
]
)
).dfAlert(
isPresented: $showSuccess,
alert: DFAlert(
title: "Account Created",
message: "Welcome! You can now sign in.",
actions: [DFAlertAction(title: "Got it", role: .cancel) { }]
)
)DFToast provides non-blocking, auto-dismissing feedback messages. Toasts are shown imperatively via DFToastQueue.shared from anywhere in the app — including async tasks — and rendered at the scene root via a single .dfToast(queue:) modifier.
@main
struct MyApp: App {
var body: some Scene {
WindowGroup {
ContentView()
.dfThemePreset(.slate)
.dfToast(queue: DFToastQueue.shared)
}
}
}DFToastQueue.shared.show("Profile updated", style: .success)
DFToastQueue.shared.show("Upload failed", style: .error)
DFToastQueue.shared.show("Syncing…", style: .info)
DFToastQueue.shared.show("Low storage", style: .warning)public enum DFToastSeverity: Sendable {
case success, error, info, warning, neutral
}
public struct DFToastMessage: Identifiable, Sendable {
public let id: UUID
public let message: String
public let severity: DFToastSeverity
public let duration: TimeInterval // default 3.0 s; set to 0 for persistent
public let action: DFToastAction? // optional tap action button
}
public struct DFToastAction: Sendable {
public let title: String
public let action: @Sendable () -> Void
}// Show with default 3-second duration
DFToastQueue.shared.show("Saved", style: .success)
// Show with custom duration (seconds)
DFToastQueue.shared.show("Indexing…", style: .info, duration: 10)
// Persistent (does not auto-dismiss)
DFToastQueue.shared.show("Offline mode active", style: .warning, duration: 0)
// With tap action
DFToastQueue.shared.show(
"New message from Alex",
style: .info,
action: DFToastAction(title: "View") { navigateToMessages() }
)
// Dismiss all queued toasts
DFToastQueue.shared.dismissAll()func uploadPhoto(_ photo: Photo) async {
do {
try await api.upload(photo)
DFToastQueue.shared.show("Photo uploaded", style: .success)
} catch {
DFToastQueue.shared.show(
"Upload failed",
style: .error,
action: DFToastAction(title: "Retry") { Task { await uploadPhoto(photo) } }
)
}
}DFButton("Copy Link") {
UIPasteboard.general.url = shareURL
DFToastQueue.shared.show("Link copied", style: .success, duration: 2)
}
.dfButtonStyle(.tinted).onChange(of: networkMonitor.isConnected) { _, connected in
if !connected {
DFToastQueue.shared.show("You're offline", style: .warning, duration: 0)
} else {
DFToastQueue.shared.dismissAll()
DFToastQueue.shared.show("Back online", style: .success, duration: 2)
}
}| Scenario | Use |
|---|---|
| Destructive action requires explicit confirmation |
DFAlert with .destructive role |
| Save/discard decision before leaving a screen |
DFAlert with multiple named actions |
| Non-blocking success confirmation |
DFToast .success
|
| Non-blocking transient error |
DFToast .error with optional Retry action |
| Background operation progress |
DFToast .info with long or persistent duration |
| An error that changes app behavior until resolved |
DFToast .warning persistent |
| Focused multi-step flow |
DFModal or DFSheet
|
| Contextual help text |
DFTooltip or DFPopover
|
All six components follow the style protocol pattern:
// Example: custom toast style
struct BannerToastStyle: DFToastStyle, Sendable {
func makeBody(configuration: DFToastStyleConfiguration) -> some View {
HStack(spacing: 12) {
DFIcon(configuration.severity.icon, color: configuration.severity.color(configuration.theme))
Text(configuration.message)
.font(configuration.theme.typography.label.font)
Spacer()
if let action = configuration.action {
Button(action.title, action: action.action)
.buttonStyle(.plain)
.foregroundColor(configuration.theme.colors.primary)
}
}
.padding(configuration.theme.spacing.md)
.background(configuration.theme.colors.surfaceElevated)
.cornerRadius(configuration.theme.radius.md)
.shadow(
color: configuration.theme.shadows.md.color,
radius: configuration.theme.shadows.md.radius,
y: configuration.theme.shadows.md.y
)
}
}See also: Buttons · Forms-and-Validation · Theming · Getting-Started