How to create a custom title bar for macOS app in SwiftUI with aligned window controls?
I’m developing a macOS app using SwiftUI and want to create a unified title bar where my app icons are perfectly aligned with the window control buttons (close, minimize, maximize). Currently, when I use .windowStyle(.hiddenTitleBar), the title bar space is still reserved, causing my content to appear below it instead of utilizing the full window space.
Current implementation:
import SwiftUI
import AppKit
@main
struct MyApp: App {
var body: some Scene {
DocumentGroup(newDocument: MyDocumentApp()) { file in
ContentView(document: file.$document, rootURL: file.fileURL)
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
.defaultSize(width: 1400, height: 900)
}
}
What’s the proper approach to create a custom title bar in SwiftUI that allows for precise alignment of custom app icons with the native window controls?
Creating a custom title bar in SwiftUI with properly aligned window controls requires working directly with NSWindow through NSWindowController, as SwiftUI’s built-in window styling options have limitations. The key is to create a transparent title bar and position your custom content precisely relative to the native window controls.
Contents
- Understanding the Problem
- NSWindow Controller Approach
- Implementing NSVisualEffectView
- Proper Window Controls Alignment
- Complete Implementation Example
- Alternative Approaches
- Troubleshooting Common Issues
Understanding the Problem
When you use .windowStyle(.hiddenTitleBar) in SwiftUI, the title bar area remains reserved for system purposes even though it’s visually hidden. This is why your content appears below it instead of utilizing the full window space. According to the Apple Developer Documentation, the title bar serves specific system functions that can’t be completely removed through SwiftUI modifiers alone.
The challenge lies in creating a custom title bar that:
- Appears unified with your app’s design
- Maintains proper functionality of window controls
- Allows precise alignment of custom elements with system controls
- Doesn’t interfere with macOS window management behaviors
NSWindow Controller Approach
The most reliable method involves subclassing NSWindowController and configuring the window directly. As demonstrated in the Reddit discussion, this approach gives you full control over the window’s appearance and behavior.
import Cocoa
import SwiftUI
class CustomWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// Configure window for custom title bar
window?.titleVisibility = .hidden
window?.titlebarAppearsTransparent = true
window?.styleMask.insert(.fullSizeContentView)
window?.isMovableByWindowBackground = true
// Remove standard title bar
window?.styleMask.remove(.titled)
// Set up custom content view
let hostingView = NSHostingView(rootView: ContentView())
window?.contentView = hostingView
}
}
This approach allows you to start with a clean slate while maintaining access to the underlying window features.
Implementing NSVisualEffectView
For a more sophisticated title bar with blur effects, use NSVisualEffectView. As shown in the GitHub example, this provides better visual integration with macOS styling.
import Cocoa
import SwiftUI
class VisualEffectWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// Create visual effect view
let visualEffect = NSVisualEffectView()
visualEffect.blendingMode = .behindWindow
visualEffect.state = .active
visualEffect.material = .underWindowBackground
// Configure window
window?.titlebarAppearsTransparent = true
window?.styleMask.insert(.fullSizeContentView)
// Create hosting view for your SwiftUI content
let hostingView = NSHostingView(rootView: ContentView())
// Add visual effect as the main content view
window?.contentView = visualEffect
visualEffect.addSubview(hostingView)
// Configure hosting view to fill the window
hostingView.translatesAutoresizingMaskIntoConstraints = false
hostingView.topAnchor.constraint(equalTo: visualEffect.topAnchor).isActive = true
hostingView.leadingAnchor.constraint(equalTo: visualEffect.leadingAnchor).isActive = true
hostingView.trailingAnchor.constraint(equalTo: visualEffect.trailingAnchor).isActive = true
hostingView.bottomAnchor.constraint(equalTo: visualEffect.bottomAnchor).isActive = true
}
}
The NSVisualEffectView provides a translucent background that integrates naturally with the macOS design language while allowing your custom title bar content to appear above it.
Proper Window Controls Alignment
Aligning your custom content with the window controls requires knowing their exact positioning. According to the NSWindow Styles documentation, the window controls appear in the top-right corner with specific positioning.
// In your SwiftUI view's body
struct ContentView: View {
var body: some View {
ZStack {
// Your main content
Color.clear
// Custom title bar overlay
VStack(spacing: 0) {
// Title bar area
HStack {
// Your app icons/content on the left
Image(systemName: "star.fill")
.font(.title2)
.padding(.leading, 20)
Spacer()
// Window controls area - align with system controls
HStack(spacing: 12) {
CustomButton(icon: "minus") { /* minimize action */ }
CustomButton(icon: "plus") { /* maximize action */ }
CustomButton(icon: "xmark") { /* close action */ }
}
.padding(.trailing, 12)
}
.frame(height: 32)
// Divider line
Rectangle()
.fill(Color.gray.opacity(0.3))
.frame(height: 1)
Spacer()
}
.background(Color.clear)
}
}
}
struct CustomButton: View {
let icon: String
let action: () -> Void
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.system(size: 12, weight: .medium))
.frame(width: 12, height: 12)
}
.buttonStyle(PlainButtonStyle())
.onHover { isHovering in
// Add hover effects
}
}
}
The key is to use the exact same padding and spacing as the system controls. The right padding of 12 points matches the system’s default spacing for window controls.
Complete Implementation Example
Here’s a complete implementation that combines all the approaches:
import SwiftUI
import AppKit
// Custom window controller
class CustomWindowController: NSWindowController {
override func windowDidLoad() {
super.windowDidLoad()
// Configure window
window?.titleVisibility = .hidden
window?.titlebarAppearsTransparent = true
window?.styleMask.insert(.fullSizeContentView)
window?.isMovableByWindowBackground = true
// Remove standard title bar styling
window?.styleMask.remove(.titled)
// Create visual effect for background
let visualEffect = NSVisualEffectView()
visualEffect.blendingMode = .behindWindow
visualEffect.state = .active
visualEffect.material = .underWindowBackground
// Set up custom content
let hostingView = NSHostingView(rootView: ContentView())
window?.contentView = visualEffect
visualEffect.addSubview(hostingView)
// Configure constraints
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.topAnchor.constraint(equalTo: visualEffect.topAnchor),
hostingView.leadingAnchor.constraint(equalTo: visualEffect.leadingAnchor),
hostingView.trailingAnchor.constraint(equalTo: visualEffect.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: visualEffect.bottomAnchor)
])
}
}
// Custom content view
struct ContentView: View {
var body: some View {
ZStack {
// Background
Color.clear
VStack(spacing: 0) {
// Custom title bar
titleBarView
// Main content area
ZStack {
Color.red.opacity(0.1)
.cornerRadius(8)
.padding()
Text("Main Content Area")
.font(.title)
.padding()
}
}
}
.edgesIgnoringSafeArea(.all)
}
private var titleBarView: some View {
HStack {
// Left side content
HStack(spacing: 8) {
Image(systemName: "star.fill")
.font(.title2)
.padding(.leading, 20)
Text("My App")
.font(.headline)
}
Spacer()
// Right side - window controls
HStack(spacing: 12) {
CustomWindowButton(icon: "minus", action: { /* minimize */ })
CustomWindowButton(icon: "plus", action: { /* maximize */ })
CustomWindowButton(icon: "xmark", action: { /* close */ })
}
.padding(.trailing, 12)
}
.frame(height: 32)
.background(Color.clear)
}
}
// Custom window button
struct CustomWindowButton: View {
let icon: String
let action: () -> Void
@State private var isHovering = false
var body: some View {
Button(action: action) {
Image(systemName: icon)
.font(.system(size: 11, weight: .medium))
.frame(width: 12, height: 12)
.foregroundColor(isHovering ? .primary : .secondary)
}
.buttonStyle(PlainButtonStyle())
.onHover { hovering in
isHovering = hovering
}
}
}
// App structure
@main
struct CustomTitleBarApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
.windowStyle(.hiddenTitleBar)
.windowResizability(.contentSize)
.defaultSize(width: 800, height: 600)
.windowToolbarStyle(.unified)
}
}
Alternative Approaches
Using TitleBarWindowStyle
According to the Apple Developer Documentation, you can use the TitleBarWindowStyle for a more integrated approach:
.windowStyle(.titleBar)
.toolbar {
ToolbarItem(placement: .automatic) {
// Custom toolbar content
}
}
Using NavigationSplitView
For apps with sidebar navigation, the Swift and AppKit Tips article suggests using NavigationSplitView with custom toolbar items:
NavigationSplitView {
// Sidebar content
} detail: {
// Main content
}
.toolbar {
ToolbarItem(placement: .navigation) {
Text("Custom Title")
.font(.system(size: 20, weight: .regular))
}
}
Troubleshooting Common Issues
Window Controls Not Responding
If your custom window controls don’t respond, make sure you’re not interfering with the hit testing. Add proper frame sizing:
CustomWindowButton(icon: "xmark", action: { /* close */ })
.frame(width: 12, height: 12)
Title Bar Not Transparent
Ensure proper configuration:
window?.titlebarAppearsTransparent = true
window?.styleMask.insert(.fullSizeContentView)
Content Not Filling Window
Check your constraints and make sure the content view fills the entire window:
hostingView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
hostingView.topAnchor.constraint(equalTo: visualEffect.topAnchor),
hostingView.leadingAnchor.constraint(equalTo: visualEffect.leadingAnchor),
hostingView.trailingAnchor.constraint(equalTo: visualEffect.trailingAnchor),
hostingView.bottomAnchor.constraint(equalTo: visualEffect.bottomAnchor)
])
Sources
- Stack Overflow - How to create a custom title bar?
- Apple Developer Documentation - TitleBarWindowStyle
- Reddit - Translucent window with transparent title bar
- GitHub - NSWindow Styles
- NSWindow Styles - Visual Effect Implementation
- Level Up Coding - SwiftUI/MacOS Customization
- Medium - Customizing SwiftUI Settings Window
- Apple Developer Documentation - Customizing window styles
Conclusion
Creating a custom title bar in SwiftUI with properly aligned window controls requires understanding the limitations of SwiftUI’s built-in window styling and working directly with AppKit components. The key takeaways are:
- Use NSWindowController for full control over window appearance and behavior
- Implement NSVisualEffectView for proper visual integration with macOS
- Align window controls precisely using the same spacing and padding as system controls
- Configure window properties like
titlebarAppearsTransparentandfullSizeContentView - Test thoroughly to ensure all window controls function correctly
By following these approaches, you can create a unified title bar that seamlessly integrates your custom app content with the native macOS window controls, providing a professional and consistent user experience.