NeuroAgent

SwiftUI Custom Title Bar: Perfect Alignment Guide

Learn how to create a custom title bar in SwiftUI with perfectly aligned window controls. This complete guide covers NSWindowController, NSVisualEffectView, and proper alignment techniques for professional macOS apps.

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:

swift
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?

NeuroAgent

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

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.

swift
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.

swift
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.

swift
// 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:

swift
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:

swift
.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:

swift
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:

swift
CustomWindowButton(icon: "xmark", action: { /* close */ })
    .frame(width: 12, height: 12)

Title Bar Not Transparent

Ensure proper configuration:

swift
window?.titlebarAppearsTransparent = true
window?.styleMask.insert(.fullSizeContentView)

Content Not Filling Window

Check your constraints and make sure the content view fills the entire window:

swift
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

  1. Stack Overflow - How to create a custom title bar?
  2. Apple Developer Documentation - TitleBarWindowStyle
  3. Reddit - Translucent window with transparent title bar
  4. GitHub - NSWindow Styles
  5. NSWindow Styles - Visual Effect Implementation
  6. Level Up Coding - SwiftUI/MacOS Customization
  7. Medium - Customizing SwiftUI Settings Window
  8. 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:

  1. Use NSWindowController for full control over window appearance and behavior
  2. Implement NSVisualEffectView for proper visual integration with macOS
  3. Align window controls precisely using the same spacing and padding as system controls
  4. Configure window properties like titlebarAppearsTransparent and fullSizeContentView
  5. 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.