Dialog (Confirmation)
A native SwiftUI .confirmationDialog action sheet that takes VoiceOver focus on presentation, with each action returning focus to the trigger because native confirmation dialogs do not restore it automatically.
Selection
The criteria an agent checks before retrieving this component.
Use when
- Use when a control offers a short set of actions to confirm or choose from, presented as an action sheet anchored to that control (e.g., confirming a destructive delete, picking one of a few actions on an item). Uses the native SwiftUI
.confirmationDialog(_:isPresented:titleVisibility:actions:message:)modifier. - Use when the choice is a small set of clearly labeled actions (typically two to four) plus a cancel.
Try a different component when
- Do not use when a brief, blocking message needs the user to acknowledge it or make a small decision before continuing (use
dialog.alert). - Do not use when the content is a larger form or multi-step task (use
dialog.modal). - Do not use when the items are commands pulled down from a control rather than a one-time confirmation or choice (use
menu.basic). - Do not use when the user is choosing a single persistent value rather than committing an action (use
select.menu).
Must Haves
Non-negotiable structure. Every generated instance must satisfy these rules.
- Use the native
.confirmationDialog(_:isPresented:titleVisibility:actions:message:)modifier so the action sheet is a real overlay that takes VoiceOver focus on presentation and blocks interaction with the rest of the screen until it is dismissed. - Provide the primary question or statement as the dialog title, and put any supporting detail in the
message:closure. - Set
titleVisibility: .visibleso the title is shown and spoken, unless the triggering context already makes the choice clear (see Customizable). - Return VoiceOver focus to the trigger on dismissal: bind the trigger with
@AccessibilityFocusStateand set it true inside every action's closure, because native confirmation dialogs do not restore focus automatically, which is an Apple platform defect (WCAG 2.4.3). Seeglobal.focus-management. - Give each action a specific label and the correct role:
.cancelfor the dismissive action and.destructivefor a destructive one, so VoiceOver and the system present them correctly. - Keep the action set short and the labels self-explanatory out of context (e.g., "Discard Draft", "Keep Editing"), not "OK"/"Yes"/"No" where the outcome is ambiguous.
- Meets the touch target size baseline in
global_rules.md(global.touch-target-size). - Meets the system focus indicator baseline in
global_rules.md(global.focus-visible).
Donts
Avoid these accessibility and UX barriers.
- Do not build a custom view as a faux action sheet (a conditional
VStackoverlay); it does not receive VoiceOver focus on display, does not block the background, and does not restore focus on close. Use the native.confirmationDialog(), ordialog.modalfor a richer custom modal. - Do not omit focus return; without
@AccessibilityFocusStateset in each action, VoiceOver focus is lost when the dialog closes, which is a gap in the native control. - Do not rely on color alone to signal a destructive action; use the
.destructiverole and a clear label, not only red text. - Do not put forms, many controls, or lengthy content in a confirmation dialog; use
dialog.modalfor that.
Customizable
Alternatives and options that give the AI agent some room to move.
- The dialog may present a single confirming action plus cancel, or several actions plus cancel, as long as every action returns focus to the trigger.
- The confirming action may carry the
.destructiverole, or the default role when the action is not destructive. - The title may be hidden with
titleVisibility: .hiddenwhen the triggering context already makes the choice clear, as long as the message or action labels still convey what is being confirmed.
Golden Pattern
The tested reference implementation. Agents start from this shape and adapt to the developer’s codebase and context.
import SwiftUI
struct DialogConfirmationDemo: View {
@State private var showingDialog = false
@AccessibilityFocusState private var triggerFocused: Bool
var body: some View {
Button("Discard Draft", role: .destructive) {
showingDialog = true
}
.accessibilityFocused($triggerFocused)
.confirmationDialog(
"Discard this draft?",
isPresented: $showingDialog,
titleVisibility: .visible
) {
// Each action returns VoiceOver focus to the trigger, since native
// confirmation dialogs do not restore it automatically.
Button("Discard Draft", role: .destructive) {
print("Draft discarded")
triggerFocused = true
}
Button("Keep Editing", role: .cancel) {
triggerFocused = true
}
} message: {
Text("Your unsent changes will be lost.")
}
}
}
Acceptance Checks
The component’s test spec — an optional body of checks for verification.
Traits & semantics
- The confirmation dialog is an overlay presented over the screen; while it is open, only its content and actions are reachable.
VoiceOver
- When the dialog opens, VoiceOver focus moves into it and the title and message are announced.
- Each action button speaks its specific label (e.g., "Discard Draft", "Keep Editing").
- When any action dismisses the dialog, VoiceOver focus returns to the trigger button.
Switch Control & Full Keyboard Access
- The dialog's actions are reachable and activatable via Switch Control and a hardware keyboard, and dismissing returns focus to the trigger.
Dynamic Type
- The dialog's title, message, and action labels scale with Dynamic Type and stay fully visible.
Visual
- Action labels are specific and do not depend on color alone. Native confirmation dialog button text contrast can fall short (an Apple platform defect); verify against your target appearances.