Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Design native macOS apps following Apple's Human Interface Guidelines and Liquid Glass design system. Use when building SwiftUI/AppKit macOS apps, validating designs, or implementing macOS-specific patterns.
.claude/skills/macos-hig-designer/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-10 | ✗→✓ | ▲ Improved | — | — |
| case-19 | ✗→✓ | ▲ Improved | — | — |
| case-16 | ✗→✓ | ▲ Improved | — | — |
| case-05 | ✓→✓ | = Same ✓ | — | — |
| case-15 | ✗→✗ | = Same ✗ | — | — |
Design native macOS applications following Apple's Human Interface Guidelines with macOS Tahoe's Liquid Glass design system.
User Request
│
├─► "Review my macOS UI code"
│ └─► Run HIG Compliance Check (Section 11)
│ └─► Report violations with fixes
│
├─► "Modernize this macOS code"
│ └─► Identify deprecated APIs
│ └─► Apply Modern API Replacements (Section 10)
│
└─► "Build [feature] for macOS"
└─► Design with HIG principles first
└─► Implement with modern SwiftUI patterns| Principle | Description | Implementation | |-----------|-------------|----------------| | Hierarchy | Visual layers through Liquid Glass translucency | Use .glassEffect(), materials, and depth | | Harmony | Concentric alignment between hardware/software | Round corners, consistent radii, flowing shapes | | Consistency | Platform conventions that adapt to context | Follow standard patterns, respect user preferences |
Liquid Glass combines transparency, reflection, refraction, and fluidity with a frosted aesthetic:
swift// macOS Tahoe Liquid Glass effect .glassEffect() // Primary Liquid Glass material .glassEffect(.regular.tinted) // Tinted variant (26.1+) // Pre-Tahoe fallback .background(.ultraThinMaterial) .background(.regularMaterial) .background(.thickMaterial)
When to use Liquid Glass:
When NOT to use:
Three-column layout for document-based and content-heavy apps:
swiftstruct ContentView: View { @State private var selection: Item.ID? @State private var columnVisibility: NavigationSplitViewVisibility = .all var body: some View { NavigationSplitView(columnVisibility: $columnVisibility) { // Sidebar (source list) List(items, selection: $selection) { item in NavigationLink(value: item) { Label(item.title, systemImage: item.icon) } } .navigationSplitViewColumnWidth(min: 180, ideal: 220, max: 300) } content: { // Content column (optional middle) ContentListView(selection: selection) } detail: { // Detail view DetailView(item: selectedItem) } } }
swift// Source list with sections List(selection: $selection) { Section("Library") { ForEach(libraryItems) { item in Label(item.name, systemImage: item.icon) .tag(item) } } Section("Collections") { ForEach(collections) { collection in Label(collection.name, systemImage: "folder") .tag(collection) .badge(collection.count) } } } .listStyle(.sidebar)
swiftstruct DocumentView: View { @State private var showInspector = true var body: some View { MainContentView() .inspector(isPresented: $showInspector) { InspectorView() .inspectorColumnWidth(min: 200, ideal: 250, max: 400) } .toolbar { ToolbarItem { Button { showInspector.toggle() } label: { Label("Inspector", systemImage: "sidebar.trailing") } } } } }
swift@main struct MyApp: App { var body: some Scene { // Main document window WindowGroup { ContentView() } .windowStyle(.automatic) .windowToolbarStyle(.unified) .defaultSize(width: 900, height: 600) .defaultPosition(.center) // Settings window Settings { SettingsView() } // Utility window Window("Inspector", id: "inspector") { InspectorWindow() } .windowStyle(.plain) .windowResizability(.contentSize) .defaultPosition(.topTrailing) // Menu bar extra MenuBarExtra("Status", systemImage: "circle.fill") { StatusMenu() } .menuBarExtraStyle(.window) } }
| Style | Use Case | |-------|----------| | .automatic | Standard app windows | | .hiddenTitleBar | Content-focused (media players) | | .plain | Utility windows, panels | | .unified | Integrated toolbar appearance | | .unifiedCompact | Compact toolbar height |
swiftWindowGroup { ContentView() } .handlesExternalEvents(matching: Set(arrayLiteral: "main")) .commands { CommandGroup(replacing: .newItem) { Button("New Document") { // Handle new document } .keyboardShortcut("n") } }
swift@main struct DocumentApp: App { var body: some Scene { DocumentGroup(newDocument: MyDocument()) { file in DocumentView(document: file.$document) } .commands { CommandGroup(after: .saveItem) { Button("Export...") { } .keyboardShortcut("e", modifiers: [.command, .shift]) } } } } struct MyDocument: FileDocument { static var readableContentTypes: [UTType] { [.plainText] } init(configuration: ReadConfiguration) throws { } func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper { } }
swift.toolbar { // Leading items (macOS places these before title) ToolbarItem(placement: .navigation) { Button(action: goBack) { Label("Back", systemImage: "chevron.left") } } // Principal (centered) ToolbarItem(placement: .principal) { Picker("View Mode", selection: $viewMode) { Label("Icons", systemImage: "square.grid.2x2").tag(ViewMode.icons) Label("List", systemImage: "list.bullet").tag(ViewMode.list) } .pickerStyle(.segmented) } // Trailing items ToolbarItemGroup(placement: .primaryAction) { Button(action: share) { Label("Share", systemImage: "square.and.arrow.up") } Button(action: toggleInspector) { Label("Inspector", systemImage: "sidebar.trailing") } } } .toolbarRole(.editor) // or .browser, .automatic
swift.commands { // Replace existing menu group CommandGroup(replacing: .newItem) { Button("New Project") { } .keyboardShortcut("n") Button("New from Template...") { } .keyboardShortcut("n", modifiers: [.command, .shift]) } // Add to existing group CommandGroup(after: .sidebar) { Button("Toggle Inspector") { } .keyboardShortcut("i", modifiers: [.command, .option]) } // Custom menu CommandMenu("Canvas") { Button("Zoom In") { } .keyboardShortcut("+") Button("Zoom Out") { } .keyboardShortcut("-") Divider() Button("Fit to Window") { } .keyboardShortcut("0") } }
swiftMenuBarExtra("App Status", systemImage: statusIcon) { VStack(alignment: .leading, spacing: 8) { Text("Status: \(status)") .font(.headline) Divider() Button("Open Main Window") { openWindow(id: "main") } Button("Quit") { NSApplication.shared.terminate(nil) } .keyboardShortcut("q") } .padding() } .menuBarExtraStyle(.window) // or .menu for simple dropdown
Always implement these when applicable:
| Action | Shortcut | Implementation | |--------|----------|----------------| | New | ⌘N | .keyboardShortcut("n") | | Open | ⌘O | .keyboardShortcut("o") | | Save | ⌘S | .keyboardShortcut("s") | | Close | ⌘W | .keyboardShortcut("w") | | Undo | ⌘Z | .keyboardShortcut("z") | | Redo | ⇧⌘Z | .keyboardShortcut("z", modifiers: [.command, .shift]) | | Cut | ⌘X | .keyboardShortcut("x") | | Copy | ⌘C | .keyboardShortcut("c") | | Paste | ⌘V | .keyboardShortcut("v") | | Select All | ⌘A | .keyboardShortcut("a") | | Find | ⌘F | .keyboardShortcut("f") | | Preferences | ⌘, | .keyboardShortcut(",") | | Hide | ⌘H | System handled | | Quit | ⌘Q | System handled |
swiftButton("Toggle Sidebar") { toggleSidebar() } .keyboardShortcut("s", modifiers: [.command, .control]) // Function keys Button("Refresh") { } .keyboardShortcut(KeyEquivalent.init(Character(UnicodeScalar(NSF5FunctionKey)!))) // Arrow keys Button("Next") { } .keyboardShortcut(.rightArrow)
| Size | Shape | Use Case | |------|-------|----------| | Mini | Rounded rect | Compact toolbars, dense UIs | | Small | Rounded rect | Secondary controls, sidebars | | Regular | Rounded rect | Primary controls (default) | | Large | Capsule | Prominent actions | | Extra Large | Capsule + Glass | Hero CTAs, onboarding |
swift// Size modifiers Button("Action") { } .controlSize(.mini) // Smallest .controlSize(.small) // Compact .controlSize(.regular) // Default .controlSize(.large) // Prominent .controlSize(.extraLarge) // Hero (macOS 15+)
swift// Primary action (prominent) Button("Save Changes") { } .buttonStyle(.borderedProminent) .controlSize(.large) // Secondary action Button("Cancel") { } .buttonStyle(.bordered) // Tertiary/link style Button("Learn More") { } .buttonStyle(.plain) .foregroundStyle(.link) // Destructive Button("Delete", role: .destructive) { } .buttonStyle(.bordered) // Toolbar button Button { } label: { Label("Add", systemImage: "plus") } .buttonStyle(.borderless)
swift// Standard text field TextField("Search", text: $query) .textFieldStyle(.roundedBorder) // Search field with tokens TextField("Search", text: $query) .searchable(text: $query, tokens: $tokens) { token in Label(token.name, systemImage: token.icon) } // Secure field SecureField("Password", text: $password) .textFieldStyle(.roundedBorder)
swiftTable(items, selection: $selection) { TableColumn("Name", value: \.name) .width(min: 100, ideal: 150) TableColumn("Date") { item in Text(item.date, format: .dateTime) } .width(100) TableColumn("Status") { item in StatusBadge(status: item.status) } .width(80) } .tableStyle(.inset(alternatesRowBackgrounds: true)) .contextMenu(forSelectionType: Item.ID.self) { selection in Button("Open") { } Button("Delete", role: .destructive) { } }
swift// Popover Button("Info") { showPopover = true } .popover(isPresented: $showPopover, arrowEdge: .bottom) { InfoView() .frame(width: 300, height: 200) .padding() } // Sheet .sheet(isPresented: $showSheet) { SheetContent() .frame(minWidth: 400, minHeight: 300) } // Alert .alert("Delete Item?", isPresented: $showAlert) { Button("Cancel", role: .cancel) { } Button("Delete", role: .destructive) { deleteItem() } } message: { Text("This action cannot be undone.") }
swift// Semantic styles (preferred) Text("Title").font(.largeTitle) // 26pt bold Text("Headline").font(.headline) // 13pt semibold Text("Subheadline").font(.subheadline) // 11pt regular Text("Body").font(.body) // 13pt regular Text("Callout").font(.callout) // 12pt regular Text("Caption").font(.caption) // 10pt regular Text("Caption 2").font(.caption2) // 10pt regular // Monospaced for code Text("let x = 1").font(.system(.body, design: .monospaced))
swift// Foreground .foregroundStyle(.primary) // Primary text .foregroundStyle(.secondary) // Secondary text .foregroundStyle(.tertiary) // Tertiary text .foregroundStyle(.quaternary) // Quaternary text // Backgrounds .background(.background) // Window background .background(.regularMaterial) // Translucent material // Accent colors .tint(.accentColor) // App accent color .foregroundStyle(.link) // Clickable links // Semantic colors Color.red // System red (adapts to light/dark) Color.blue // System blue
swift// Materials (adapt to background content) .background(.ultraThinMaterial) // Most transparent .background(.thinMaterial) .background(.regularMaterial) // Default .background(.thickMaterial) .background(.ultraThickMaterial) // Least transparent // Vibrancy in sidebars List { } .listStyle(.sidebar) .scrollContentBackground(.hidden) .background(.ultraThinMaterial)
swift// Standard spacing values VStack(spacing: 8) { } // Standard VStack(spacing: 16) { } // Section spacing VStack(spacing: 20) { } // Group spacing // Padding .padding(8) // Tight .padding(12) // Standard .padding(16) // Comfortable .padding(20) // Spacious // Content margins .contentMargins(16) // Uniform margins .contentMargins(.horizontal, 20) // Horizontal only
swift// Respect toolbar safe area .safeAreaInset(edge: .top) { ToolbarContent() } // Ignore safe area for backgrounds .ignoresSafeArea(.container, edges: .top) // Content that should avoid toolbar .safeAreaPadding(.top)
swift// Minimum 44x44 points for clickable elements Button { } label: { Image(systemName: "gear") } .frame(minWidth: 44, minHeight: 44) // Use contentShape for larger hit areas RoundedRectangle(cornerRadius: 8) .frame(width: 200, height: 100) .contentShape(Rectangle()) .onTapGesture { }
swift// Responsive to window size GeometryReader { geometry in if geometry.size.width > 600 { HStack { content } } else { VStack { content } } } // Grid that adapts LazyVGrid(columns: [ GridItem(.adaptive(minimum: 150, maximum: 250)) ], spacing: 16) { ForEach(items) { ItemView(item: $0) } }
swift// Labels and hints Button { } label: { Image(systemName: "plus") } .accessibilityLabel("Add item") .accessibilityHint("Creates a new item in your library") // Grouping related elements VStack { Text(item.title) Text(item.subtitle) } .accessibilityElement(children: .combine) // Custom actions .accessibilityAction(named: "Delete") { deleteItem() }
swift// Focus management @FocusState private var focusedField: Field? TextField("Name", text: $name) .focused($focusedField, equals: .name) .onSubmit { focusedField = .email } // Focusable custom views .focusable() .onMoveCommand { direction in handleArrowKey(direction) }
swift// Scales with user preference Text("Content") .dynamicTypeSize(.large ... .accessibility3) // Fixed size when necessary (use sparingly) Text("Fixed") .dynamicTypeSize(.large)
swift@Environment(\.accessibilityReduceMotion) var reduceMotion .animation(reduceMotion ? .none : .spring(), value: isExpanded) // Alternative non-animated transitions .transaction { transaction in if reduceMotion { transaction.animation = nil } }
swift@Environment(\.colorSchemeContrast) var contrast // Increase contrast when needed .foregroundStyle(contrast == .increased ? .primary : .secondary)
| Deprecated | Modern | Notes | |------------|--------|-------| | NavigationView | NavigationSplitView / NavigationStack | Split for macOS, Stack for simple flows | | .navigationViewStyle(.columns) | NavigationSplitView | Built-in column support | | List { }.listStyle(.sidebar) with NavigationLink | NavigationSplitView sidebar | Proper split view behavior | | .toolbar { ToolbarItem(...) } in detail | .toolbar on NavigationSplitView | Toolbar applies to correct scope | | NSWindowController | WindowGroup / Window | Pure SwiftUI window management | | NSMenu / NSMenuItem | .commands { } / CommandMenu | Declarative menus | | NSToolbar | .toolbar { } | SwiftUI toolbar API | | NSTouchBar | .touchBar { } | SwiftUI Touch Bar | | NSOpenPanel.begin() | .fileImporter() | SwiftUI file dialog | | NSSavePanel.begin() | .fileExporter() | SwiftUI save dialog | | .background(Color.clear) for materials | .background(.regularMaterial) | Proper material support | | Custom blur effects | .glassEffect() (Tahoe) | Native Liquid Glass |
swift// Wrap AppKit view struct NSViewWrapper: NSViewRepresentable { func makeNSView(context: Context) -> NSView { // Create and configure NSView } func updateNSView(_ nsView: NSView, context: Context) { // Update when SwiftUI state changes } } // Access NSWindow .background(WindowAccessor { window in window?.titlebarAppearsTransparent = true })
.glassEffect() or appropriate materialNavigationSplitView used for multi-column layoutsDocumentGroup.borderedProminent.destructive role┌─────────────────────────────────────────────────────┐
│ Mini/Small/Medium │ Large/XLarge │
│ ┌──────────────────┐ │ ╭──────────────────╮ │
│ │ Rounded Rect │ │ │ Capsule │ │
│ └──────────────────┘ │ ╰──────────────────╯ │
│ │ │
│ Compact layouts │ Hero actions │
│ Toolbars, sidebars │ Onboarding, CTAs │
└─────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────┐
│ .automatic Standard window with titlebar │
│ .hiddenTitleBar Full content, titlebar hidden │
│ .plain No chrome, utility panels │
│ .unified Toolbar merges with titlebar │
│ .unifiedCompact Compact unified toolbar │
└─────────────────────────────────────────────────────┘┌─────────────┬───────────────┬─────────────────────┐
│ Sidebar │ Content │ Detail │
│ │ │ │
│ Source │ List or │ Selected item │
│ List │ Grid │ properties │
│ │ │ │
│ Collections│ Items │ Inspector panel │
│ Folders │ Browse │ Edit view │
│ │ │ │
│ Min: 180 │ Flexible │ Min: 300 │
│ Max: 300 │ │ Ideal: 400+ │
└─────────────┴───────────────┴─────────────────────┘
NavigationSplitView (three-column)| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +14 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.