Customize macOS 15+ SwiftUI windows and scene behavior using Window, WindowGroup, and macOS window modifiers. Use when styling or hiding window toolbars and titles, extending drag regions with WindowDragGesture, replacing window backgrounds with materials, disabling minimize or restoration for utility windows, setting default or ideal window placement from content/display size, creating borderless windows, or tuning default launch behavior.
Use this skill to tailor each SwiftUI window to its job. Start by identifying
which scene owns the window (Window, WindowGroup, or a dedicated utility
scene), then customize the toolbar/title area, background material, resize and
restoration behavior, and initial or zoomed placement.
Prefer scene and window modifiers over ad hoc AppKit bridges when SwiftUI offers the behavior directly. Keep each window purpose-built: a main browser window, an About window, and a media player window usually want different chrome, resizability, restoration, and placement rules.
These APIs are macOS 15+ SwiftUI window/scene customizations. For older deployment targets, expect to use more AppKit bridging or availability guards.
build-run-debug to verify the result in
a real foreground .app bundle.appkit-interop
for a narrow NSWindow bridge rather than spreading AppKit through the view
tree..toolbar(removing: .title) when the window title should stay associated
with the window for accessibility and menus, but not be visibly drawn in the
title bar..toolbarBackgroundVisibility(.hidden, for: .windowToolbar) when large
media or hero content should visually extend to the top edge of the window..toolbarVisibility(.hidden, for: .windowToolbar) instead.WindowDragGesture() to extend the draggable area into your content..allowsWindowActivationEvents(true) so clicking
and immediately dragging a background window still activates and moves it..containerBackground(.thickMaterial, for: .window) when a utility window
or About window should replace the default window background with a subtle
frosted material..windowMinimizeBehavior(.disabled) for always-reachable utility windows
such as a custom About window where minimizing adds little value..restorationBehavior(.disabled) for windows that should not reopen on
next launch, such as About panels, transient support/info windows, or
first-run welcome surfaces.restorationBehavior(...) only when a specific window should
intentionally opt into or out of that system behavior..defaultLaunchBehavior(.presented) for windows that should appear first
on launch, such as a welcome window, and choose that behavior intentionally
rather than relying on side effects from scene creation order..defaultWindowPlacement { content, context in ... } to control the
initial size and optional position of newly opened windows.content.sizeThatFits(.unspecified) to get
the content's ideal size.context.defaultDisplay.visibleRect to get the display's usable region
after accounting for the menu bar and Dock.WindowPlacement(size: size) with a size clamped to the visible rect
when media or document content may be larger than the display. If no position
is provided, the window is centered by default..windowIdealPlacement { content, context in ... } to control what
happens when the user chooses Zoom from the Window menu or Option-clicks the
green toolbar button. For media windows, preserve aspect ratio and grow to the
largest size that fits the display..windowStyle(.plain) for borderless or highly custom chrome windows, but
make sure the content still provides a clear drag/move affordance and visible
context.WindowGroup("Destination Video") {
CatalogView()
.toolbar(removing: .title)
.toolbarBackgroundVisibility(.hidden, for: .windowToolbar)
}
Window("About", id: "about") {
AboutView()
.toolbar(removing: .title)
.toolbarBackgroundVisibility(.hidden, for: .windowToolbar)
.containerBackground(.thickMaterial, for: .window)
}
.windowMinimizeBehavior(.disabled)
.restorationBehavior(.disabled)
WindowGroup("Player", for: Video.self) { $video in
PlayerView(video: video)
}
.defaultWindowPlacement { content, context in
let idealSize = content.sizeThatFits(.unspecified)
let displayBounds = context.defaultDisplay.visibleRect
let fittedSize = clampToDisplay(idealSize, displayBounds: displayBounds)
return WindowPlacement(size: fittedSize)
}
.windowIdealPlacement { content, context in
let idealSize = content.sizeThatFits(.unspecified)
let displayBounds = context.defaultDisplay.visibleRect
let zoomedSize = zoomToFit(idealSize, displayBounds: displayBounds)
let position = centeredPosition(for: zoomedSize, in: displayBounds)
return WindowPlacement(position, size: zoomedSize)
}
PlayerView(video: video)
.overlay(alignment: .top) {
Color.clear
.frame(height: 48)
.contentShape(Rectangle())
.gesture(WindowDragGesture())
.allowsWindowActivationEvents(true)
}
Window("Welcome", id: "welcome") {
WelcomeView()
}
.windowStyle(.plain)
.defaultLaunchBehavior(.presented)
content.sizeThatFits(.unspecified) and
context.defaultDisplay.visibleRect when content/display size matters..toolbar(removing: .title) just to hide a title you forgot to set.
Keep the underlying window title meaningful.NSWindow mutation before checking whether
.windowMinimizeBehavior, .restorationBehavior, .defaultWindowPlacement,
.windowIdealPlacement, .windowStyle, or .defaultLaunchBehavior already
solve the problem.swiftui-patterns for broader scene, commands, settings, sidebar,
and inspector architecture.liquid-glass when the main question is modern macOS visual treatment,
Liquid Glass, or system material adoption.appkit-interop if a custom window behavior truly requires NSWindow,
NSPanel, or responder-chain control.build-run-debug to launch and verify the resulting windows.