A Swift package with the shared UI foundation for macOS menu bar apps, so they all read as one design language: a borderless floating panel, Liquid Glass chrome, design tokens, and a small set of reusable SwiftUI components.
What it gives you: the panel that drops down from the menu bar (window setup, glass background, sizing, positioning, outside-click dismissal), plus components to fill it — headers, icon buttons, meters, field labels — on top of shared spacing/radius/typography/color tokens. Helpers for user notifications and localization come along, since menu bar apps tend to need both.
What it does not do: it is not an app template. You still create the NSStatusItem, own the app delegate, and write your own screens; MenuBarKit only gives you the panel and the pieces inside it.
- macOS 26 (Tahoe) or newer, at build and run time — the panel is built on SwiftUI's Liquid Glass (
glassEffect), which is macOS 26 API - Xcode 26 / Swift 6.2
- No external dependencies
Add the package to your app's Package.swift:
let package = Package(
name: "MyApp",
platforms: [.macOS(.v26)], // required: MenuBarKit itself is macOS 26+
dependencies: [
.package(url: "https://github.com/gabriarceus/MenuBarKit.git", from: "1.0.0")
],
targets: [
.executableTarget(name: "MyApp", dependencies: ["MenuBarKit"])
]
)In an Xcode project instead: File ▸ Add Package Dependencies… and paste the same URL.
A menu bar app is an accessory app (no Dock icon, no main window): set LSUIElement to true in its Info.plist, or call NSApp.setActivationPolicy(.accessory) at launch. From there, the app delegate owns a status item and a MenuBarPanel:
import AppKit
import MenuBarKit
import SwiftUI
enum MyScreen: Hashable { case main, settings }
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private var statusItem: NSStatusItem!
private var panel: MenuBarPanel!
private let navigation = PanelNavigation<MyScreen>(initial: .main)
func applicationDidFinishLaunching(_ notification: Notification) {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.image = NSImage(systemSymbolName: "gauge", accessibilityDescription: nil)
statusItem.button?.target = self
statusItem.button?.action = #selector(toggle)
panel = MenuBarPanel(
rootView: PanelChrome { MyRootView(navigation: navigation) },
width: Theme.defaultPanelWidth
)
panel.onDismiss = { [weak self] in self?.navigation.screen = .main }
}
@objc private func toggle() {
if panel.isVisible {
panel.dismiss()
} else if let button = statusItem.button, let window = button.window {
// Top-left corner of the panel: just below the status item, aligned to its left edge.
let onScreen = window.convertToScreen(button.convert(button.bounds, to: nil))
panel.present(at: NSPoint(x: onScreen.minX, y: onScreen.minY - 4))
}
}
}Wrapping the root view in PanelChrome is what gives the panel its glass background and rounded shape — MenuBarPanel itself is transparent and borderless.
| Type | Purpose |
|---|---|
MenuBarPanel |
Borderless, non-activating floating NSPanel that presents popover-style content without anchoring to a view — it stays put even if a menu bar manager (Ice, Bartender, …) hides the status icon while open. Sizes itself from the SwiftUI content, keeps the top-left corner pinned while growing, dismisses on clicks outside the app, and can become key so text fields work. |
PanelChrome |
Liquid Glass chrome for the panel content. Optional tint: washes a color over the glass while keeping its translucency (the color's opacity controls the strength). |
PanelNavigation<Screen> |
Observable current-screen state (@Published var screen), drivable from both SwiftUI (back button) and the panel owner (reset on dismiss). |
Theme — shared tokens: Spacing (xs/s/m/l), Radius (field/card/panel), Colors (status colors + bar track), Typography (row title/value, field label, footnote), and defaultPanelWidth.
SystemAccentObserver — ObservableObject publishing the current macOS accent color as color, updated live when it changes in System Settings (the "Multicolor" accent resolves to the default blue).
| Component | Purpose |
|---|---|
PanelHeader |
Header row: optional leading accessory, headline title, trailing actions. |
GlassIconButton |
Small capsule icon button with an interactive glass background; the primary-based tint lightens in dark mode and darkens in light mode, so it stays visible on any theme. |
MeterRow / MeterBar |
Metered quantity (title, tinted value, capsule bar, optional footnote). The bar keeps its tint when the window is not key, unlike a tinted ProgressView. |
SectionLabel |
Small field label; renders its text uppercase, so callers pass normal-case strings. |
inputChrome(focused:) |
View modifier wrapping a text input in a bordered container with an accent highlight on focus. |
Notifier — UNUserNotificationCenter wrapper: requestAuthorizationIfNeeded() with a published authorizationDenied flag, immediate send(title:body:), and re-drivable one-shot schedule(title:body:at:identifier:) / cancelScheduled(identifier:) (plus cancelScheduled(prefix:excluding:) to clear a family of pending notifications). Every entry point no-ops when the process is not running from a .app bundle, because the notification center aborts under swift run.
Localizer — resolves UI strings from a bundle's .lproj tables, with an optional language override (empty/nil follows the system). Missing keys fall back to the key itself. Inject it once per app via EnvironmentValues.localizer and read it in views with @Environment(\.localizer).
Bundle.languageOptions(systemLabel:) — lists a bundle's localizations (each in its own name) plus a leading system-follow entry, ready to drive a language Picker via AppLanguageOption. Each app keeps its own Localizable.strings under Resources/*.lproj; adding a language is a copy-and-translate with no code change.
Bundle.resourceBundle(named:fallback:) — finds a SwiftPM resource bundle inside the built .app, falling back to Bundle.module when running straight from the build directory (the fallback is an autoclosure on purpose: Bundle.module probes an absolute build path that must not be touched in a shipped app).
Behaviour that looks arbitrary but is deliberate:
- Window shadow vs glass: SwiftUI
glassEffecton a borderless clear panel leaves faintly opaque pixels across the hosting view's full rectangular bounds, so AppKit casts the window shadow for a rectangle — a dark rim with squared corners.MenuBarPanelmasks the hosting view's layer to the panel corner radius and invalidates the shadow after presenting/resizing. - Positioning: content-driven resizes re-anchor the top-left corner (
setFrameTopLeftPointondidResize), so the panel grows downward instead of moving its top edge. - Plan B (not implemented): if glass sampling ever misbehaves on the borderless panel (edge artifacts, missing shadow), the escape hatch is to bridge AppKit's
NSGlassEffectViewas the content wrapper instead of theglassEffectmodifier.
Apps built on MenuBarKit (e.g. DisplayAnchor) declare a conditional dependency: a sibling MenuBarKit checkout is used automatically when present, otherwise the package is resolved from GitHub. So cloning this repo next to an app is enough to edit the library and rebuild the app with no manifest change:
git-projects/
├── MenuBarKit/
└── DisplayAnchor/
SwiftPM caches the evaluated manifest, so after adding or removing the sibling checkout run swift package reset once in the app (or touch Package.swift) to make it re-evaluate. Day to day, with the sibling always present, nothing extra is needed.
API changes: commit here, then tag a new semver (git tag 1.1.0 && git push origin 1.1.0) so outside consumers pick it up. Local builds don't wait for a tag.
MIT — see LICENSE.