Learn · 05 of 09

Presentations.

TerminalUI ships four presentation modifiers. Each takes a state binding (typically a Bool, optional, or Identifiable) and renders a content closure when the state says "show":

  • .alert(...) — small, focused, OK/Cancel-shaped.
  • .confirmationDialog(...) — multi-choice, primary action often .destructive.
  • .sheet(...) — full-overlay modal with arbitrary content.
  • .toast(...) — transient, non-blocking, no dismissal required.

Each conforms to ActionScope, so commands declared on a presentation modifier are active only while the presentation is on screen — exactly the semantics you want for "Cancel" and "Deploy" inside a confirmation dialog.

All four in one dashboard.

A "deploy" flow that needs every shape: a sheet for detail, a confirmation dialog for the destructive action, an alert for failure, and a toast for the success notice.

ReleaseApp.swift swift
import TerminalUI
import TerminalUICLI

@main
struct ReleaseApp: App {
  var body: some Scene {
    WindowGroup("Releases") {
      ReleaseDashboard()
    }
  }
}

struct ReleaseDashboard: View {
  @State private var showDetail = false
  @State private var confirmDeploy = false
  @State private var deployError: String?
  @State private var lastNotice: String?

  var body: some View {
    VStack(alignment: .leading, spacing: 1) {
      Button("Show details") { showDetail = true }
      Button("Deploy") { confirmDeploy = true }
        .keyboardShortcut("d", modifiers: .command)
    }
    // 1. sheet — modal overlay, dismissible.
    .sheet(isPresented: $showDetail) {
      ReleaseDetail()
        .padding(.init(horizontal: 1, vertical: 0))
    }
    // 2. confirmationDialog — choices with a primary destructive action.
    .confirmationDialog(
      "Deploy to production?",
      isPresented: $confirmDeploy
    ) {
      Button("Deploy", role: .destructive) { startDeploy() }
      Button("Cancel", role: .cancel) {}
    } message: {
      Text("This will replace the live build. Confirm to proceed.")
    }
    // 3. alert — error state binding to an optional model.
    .alert(
      "Deploy failed",
      isPresented: Binding(
        get: { deployError != nil },
        set: { if !$0 { deployError = nil } }
      ),
      presenting: deployError
    ) { _ in
      Button("OK") { deployError = nil }
    } message: { error in
      Text(error)
    }
    // 4. toast — transient, non-blocking.
    .toast(message: $lastNotice, style: SuccessToastStyle())
  }

  func startDeploy() {
    // ... do the work ...
    lastNotice = "Deploy queued."
  }
}

struct ReleaseDetail: View {
  @Environment(\.dismiss) private var dismiss

  var body: some View {
    VStack(alignment: .leading, spacing: 0) {
      Text("Release 1.4.0").bold()
      Text("Built 12 minutes ago.")
      Divider()
      Button("Close") { dismiss() }
    }
  }
}

Why presentations live at the root

All four modifiers are root-hoisted. The runtime collects presentation declarations during ordinary resolve, then composes overlay roots after the base tree has resolved. The base view tree never reflows when an overlay opens; opening or dismissing a presentation is transparent to the underlying selection / scroll / focus state unless the action itself mutates that state.

Practical consequence: state owned inside a presentation body is ephemeral — it lives only while the overlay is on screen. Hoist editor state above the presentation if it must survive dismissal. See Runtime for the full ordering rules.

Binding shapes

Each modifier accepts a state binding:

  • isPresented: Binding<Bool> — the simplest case, used by all four modifiers.
  • item: Binding<Identifiable?> — the variant most useful for "show detail for the selected row." The framework re-presents when the identity changes, not just when the binding toggles.
  • presenting: T? — used by .alert for optional payloads (e.g. an error model). The presence of the value drives presentation; the closure receives the unwrapped value.

Dismissal

Three ways to dismiss:

  • Set the state binding to false / nil. The canonical path. Mutating the source of truth is what closes the overlay.
  • Read @Environment(\.dismiss) inside the presentation body. Calling it does the binding mutation for you. Useful for "Close" buttons that don't need to know which Boolean drives them.
  • The framework dismisses on Escape for alert, confirmationDialog, and sheet. Toasts dismiss themselves when their state binding clears (or when the surrounding style's timeout fires).

Scoped commands on presentations

Because each presentation modifier conforms to ActionScope, you can attach .keyCommand / .paletteCommand / .toolbar directly to one. The bindings are active while the overlay is on the focus chain — i.e. while it's on screen — and silently inactive otherwise. Same shallowest-wins semantics as elsewhere; see Tutorial 03.

Where to go next