# Macos Settings UI

> Build a proper macOS settings/preferences window with liquid glass support for macOS 26 (Tahoe). Use this skill whenever the user asks to create a settings window, preferences UI, settings view, or preferences pane for a macOS app. Also trigger when the user mentions "liquid glass settings", "NavigationSplitView settings", "grouped Form settings", "macOS settings layout", or wants to add/fix/rebuild a settings screen in any native macOS SwiftUI app. This includes phrases like "add settings to my app", "create a preferences window", "my settings look wrong", "fix my settings UI", or even just "I need a settings view". If the user is building a macOS app and mentions settings or preferences in any context, use this skill.

- Skill: `fayazara/macos-settings-ui` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add fayazara/macos-settings-ui`
- Raw SKILL.md: https://api.skillmd.com/api/skills/fayazara/macos-settings-ui/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: fayazara (https://skillmd.com/u/fayazara)
- Updated: 2026-09-09
- Page: https://skillmd.com/skills/fayazara/macos-settings-ui

---


# macOS Settings UI with Liquid Glass

This skill produces a native macOS settings window that follows Apple's macOS 26 design guidelines. The result is a sidebar + detail NavigationSplitView with liquid glass window chrome, back/forward toolbar navigation, grouped Form sections with transparent backgrounds, and proper scroll edge effects.

## Architecture Overview

The settings UI is composed of 3 layers:

1. **Window Controller** (`SettingsWindowController.swift`) — An `NSWindowController` that creates the `NSWindow` programmatically with `.fullSizeContentView`. This is what gives the window rounded liquid glass corners. You cannot get this effect from a SwiftUI `Window` scene.

2. **Root View** (`SettingsView.swift`) — A `NavigationSplitView` with a sidebar list and detail pane. Includes back/forward navigation history in the toolbar, which also forces the creation of an `NSToolbar` (required for the liquid glass title bar treatment).

3. **Detail Panes** (one file per tab) — Each pane uses `Form { Section(...) { ... } }.formStyle(.grouped).scrollContentBackground(.hidden)`.

## Why NSWindowController Instead of SwiftUI Window Scene

SwiftUI's declarative `Window` scene does not expose the `NSWindow` style mask. The `.fullSizeContentView` flag must be set at window creation time for macOS 26 to render the liquid glass chrome (rounded corners, translucent sidebar, blurred title bar). Trying to inject it later via `NSViewRepresentable` is unreliable because SwiftUI resets the window's configuration.

The `NSWindowController` approach also lets you control the toolbar style, frame autosave, minimum size, and delegate lifecycle directly.

## Critical Modifiers

Every detail pane MUST have these three modifiers on its `Form`:

```swift
.formStyle(.grouped)
.scrollContentBackground(.hidden)
.contentMargins(.top, 8, for: .scrollContent)
```

- `.formStyle(.grouped)` — gives the native inset rounded-rect section appearance
- `.scrollContentBackground(.hidden)` — makes the form background transparent so the liquid glass window chrome shows through. Without this, you get an opaque white/dark background that breaks the glass effect.
- `.contentMargins(.top, 8)` — adds breathing room between the toolbar and the first section

The sidebar List MUST have:

```swift
.listStyle(.sidebar)
.scrollEdgeEffectStyleSoftIfAvailable()  // macOS 26 progressive blur at scroll edges
.navigationTitle("Settings")
```

The NavigationSplitView MUST have:

```swift
NavigationSplitView(columnVisibility: .constant(.all))  // sidebar always visible
// ...
.navigationTitle("Settings")
.navigationSplitViewStyle(.balanced)
```

## Reference Implementation

Read the reference files for complete, working code:

- `references/SettingsWindowController.swift` — The window controller (copy as-is, adapt the activation policy calls to your app)
- `references/SettingsView.swift` — The root view with sidebar, detail routing, and navigation history
- `references/ExampleDetailPane.swift` — A template detail pane showing common control patterns (Toggle, Picker, Slider, LabeledContent)

## Step-by-Step: Adding Settings to a New App

### 1. Create the Tab Enum

Define your settings categories. Each case needs a title and SF Symbol icon:

```swift
enum SettingsTab: String, CaseIterable, Identifiable {
    case general
    case appearance
    case about

    var id: Self { self }

    var title: String {
        switch self {
        case .general: "General"
        case .appearance: "Appearance"
        case .about: "About"
        }
    }

    var systemImage: String {
        switch self {
        case .general: "gearshape"
        case .appearance: "paintbrush"
        case .about: "info.circle"
        }
    }
}
```

### 2. Create the Window Controller

Copy `references/SettingsWindowController.swift` into your project. Adapt:
- The initial `contentRect` size (default `700x540` is good for most apps)
- The `minSize` (default `620x460`)
- The activation policy calls (`AppActivationPolicy.enter()/leave()`) — if your app isn't a menu-bar-only app, remove these

### 3. Create the Root Settings View

Copy `references/SettingsView.swift`. Adapt:
- The `SettingsTab` enum cases to match your categories
- The `SettingsDetailView` switch to return your panes

### 4. Create Detail Panes

For each tab, create a pane file. Use this template:

```swift
import SwiftUI

struct GeneralSettingsPane: View {
    @AppStorage("someKey") private var someValue = false

    var body: some View {
        Form {
            Section("Section Name") {
                Toggle("Toggle label", isOn: $someValue)
            }
        }
        .formStyle(.grouped)
        .scrollContentBackground(.hidden)
        .contentMargins(.top, 8, for: .scrollContent)
    }
}
```

### 5. Open Settings from Your App

From a menu bar, button, or anywhere:

```swift
SettingsWindowController.show(tab: .general)
```

Do NOT use a SwiftUI `Window` scene. Do NOT use `openWindow(id:)`. The window controller handles everything.

### 6. Remove Any SwiftUI Window Scene

If you previously had a `Window("Settings", id: "SETTINGS")` scene in your `App` struct, remove it entirely. The `SettingsWindowController` replaces it.

## Common Control Patterns Inside Form Sections

**Toggle with description:**
```swift
Toggle(isOn: $value) {
    VStack(alignment: .leading, spacing: 2) {
        Text("Primary label")
        Text("Description text explaining what this does.")
            .font(.caption)
            .foregroundStyle(.secondary)
    }
}
.toggleStyle(.switch)
```

**Picker (dropdown):**
```swift
Picker("Label", selection: $value) {
    Text("Option A").tag(OptionEnum.a)
    Text("Option B").tag(OptionEnum.b)
}
.pickerStyle(.menu)
```

**Picker (segmented):**
```swift
Picker("Label", selection: $value) {
    ForEach(SomeEnum.allCases) { item in
        Text(item.title).tag(item)
    }
}
.pickerStyle(.segmented)
```

**Slider with value label:**
```swift
LabeledContent("Size") {
    HStack(spacing: 12) {
        Slider(value: $size, in: 24...96, step: 2)
            .frame(width: 180)
        Text("\(Int(size)) pt")
            .monospacedDigit()
            .foregroundStyle(.secondary)
            .frame(width: 46, alignment: .trailing)
    }
}
```

**Button row:**
```swift
HStack(spacing: 8) {
    Button("Action") { doSomething() }
        .controlSize(.small)
    Button("Reset") { reset() }
        .controlSize(.small)
        .disabled(isDefault)
}
```

## Menu-Bar-Only Apps

If your app uses `NSApp.setActivationPolicy(.accessory)` (no Dock icon), you need activation policy management so the settings window brings the app to the foreground. The reference `SettingsWindowController` calls `AppActivationPolicy.enter()` on show and `.leave()` on close. Implement this as a simple reference-counting wrapper:

```swift
@MainActor
enum AppActivationPolicy {
    private static var count = 0

    static func enter() {
        count += 1
        NSApp.setActivationPolicy(.regular)
        NSApp.activate(ignoringOtherApps: true)
    }

    static func leave() {
        count = max(0, count - 1)
        guard count == 0 else { return }
        Task { @MainActor in
            NSApp.setActivationPolicy(.accessory)
        }
    }
}
```

If your app always shows in the Dock, remove the `AppActivationPolicy` calls from the window controller and just use `NSApp.activate(ignoringOtherApps: true)` in `showWindow`.

## macOS Version Compatibility

The `scrollEdgeEffectStyle(.soft)` API is macOS 26+ only. Always wrap it in an availability check:

```swift
private extension View {
    @ViewBuilder
    func scrollEdgeEffectStyleSoftIfAvailable() -> some View {
        if #available(macOS 26.0, *) {
            scrollEdgeEffectStyle(.soft, for: .all)
        } else {
            self
        }
    }
}
```

The rest of the pattern (NSWindowController with `.fullSizeContentView`, grouped Form, NavigationSplitView) works on macOS 14+.

