macOS menu bar
Build the native macOS menu bar from Compose, with items, keyboard shortcuts, icons, badges, and submenus that react to state.
menu-macos lets you build the native macOS menu bar — the strip at the top of the
screen, not inside your window — from Compose. You declare menus and items as a tree,
and the native NSMenu is rebuilt whenever the state it reads changes.
This module only does anything on macOS. NativeMenuBar is a no-op on other platforms,
so you can leave the call in shared code.
Add the dependency
dependencies {
implementation("dev.nucleusframework:nucleus.menu-macos:2.0.7")
}This pulls in the sf-symbols constants
transitively, so you can reference symbols by name without misspelling them.
Build a menu bar
Call NativeMenuBar from a composable near your application root. The first Menu
call becomes the application menu (the bold entry named after your app).
import androidx.compose.runtime.Composable
import dev.nucleusframework.menu.macos.*
import dev.nucleusframework.sfsymbols.SFSymbolObjectsAndTools
import kotlin.system.exitProcess
@Composable
fun App() {
NativeMenuBar {
Menu("MyApp") {
Item("About MyApp") { showAbout() }
Separator()
Item("Quit", shortcut = NativeKeyShortcut("q")) { exitProcess(0) }
}
Menu("File") {
Item(
"New",
shortcut = NativeKeyShortcut("n"),
icon = NsMenuItemImage.SystemSymbol(SFSymbolObjectsAndTools.DOCUMENT_BADGE_PLUS),
) { newDocument() }
Item(
"Open…",
shortcut = NativeKeyShortcut("o"),
icon = NsMenuItemImage.SystemSymbol(SFSymbolObjectsAndTools.FOLDER),
) { open() }
}
MenuHelp("Help") {
Item("Documentation") { openDocs() }
}
}
// … window content
}How it works
NativeMenuBar saves the current menu bar on first composition, then installs a new
NSMenu built from your DSL. The builder lambda is a normal composable body: it runs
during composition, and the native menu is rebuilt whenever a state read inside it
changes. When the composable leaves the composition, the saved menu bar is restored.
Because the menu is tied to the composition, host NativeMenuBar from your application
root rather than from a window's content. That way the menu survives when windows open
and close.
Menus declared with MenuWindow and MenuHelp are handed to AppKit as the well-known
Window and Help menus. macOS then populates them itself — the window list and "Bring All
to Front" for Window, and a search field at the top of Help.
API reference
Add menus and submenus
Menu adds a top-level menu at the bar level, or a submenu inside another menu. Submenus
nest to any depth.
NativeMenuBar {
Menu("Edit") {
Item("Undo", shortcut = NativeKeyShortcut("z")) { undo() }
Item("Redo", shortcut = NativeKeyShortcut("z", shift = true)) { redo() }
Separator()
Menu("Find") {
Item("Find…", shortcut = NativeKeyShortcut("f")) { find() }
Item("Find Next", shortcut = NativeKeyShortcut("g")) { findNext() }
}
}
}Use MenuWindow and MenuHelp for the standard Window and Help menus. Their content is
optional — macOS fills them in.
Add items
Item adds a regular menu item. The trailing lambda is the click callback, dispatched on
the Swing event dispatch thread. Common parameters: enabled, shortcut, icon,
state, badge, subtitle, toolTip, and tag.
Item(
"Export…",
shortcut = NativeKeyShortcut("e", shift = true),
subtitle = "Save a copy as PDF",
enabled = document != null,
) { export() }Add checkboxes and radio buttons
CheckboxItem and RadioButtonItem map their boolean state to the item's checkmark for
you. Manage the selection yourself.
var darkMode by remember { mutableStateOf(false) }
NativeMenuBar {
Menu("View") {
CheckboxItem(
"Dark mode",
checked = darkMode,
onCheckedChange = { darkMode = it },
)
Separator()
RadioButtonItem("Light", selected = !darkMode) { darkMode = false }
RadioButtonItem("Dark", selected = darkMode) { darkMode = true }
}
}If you need finer control, Item accepts a state of NsMenuItemState.OFF,
NsMenuItemState.ON, or NsMenuItemState.MIXED directly.
Add separators, section headers, and search
Menu("History") {
SearchField(placeholder = "Search history")
SectionHeader("Today")
Item("nucleusframework.dev") { open(url) }
Separator()
SectionHeader("Earlier")
Item("developer.apple.com") { open(url) }
}SearchField filters the menu's other items by title as you type. SectionHeader
requires macOS 14+ and falls back to a disabled item on older systems.
Set keyboard shortcuts
NativeKeyShortcut defaults to the Command (⌘) modifier. Add shift, option,
control, or function as needed, and use NativeKey constants for non-printable keys.
Item("Save", shortcut = NativeKeyShortcut("s")) { save() }
Item("Save As…", shortcut = NativeKeyShortcut("s", shift = true)) { saveAs() }
Item("Cancel", shortcut = NativeKeyShortcut(NativeKey.ESCAPE, command = false)) { cancel() }Add icons
NsMenuItemImage sources an item's image from an SF Symbol, a named AppKit image, or a
file path.
Item("Settings", icon = NsMenuItemImage.SystemSymbol(SFSymbolGeneral.GEAR)) { openSettings() }The sf-symbols module exposes each symbol category — SFSymbolGeneral,
SFSymbolObjectsAndTools, SFSymbolPower, and more — as typed constants.
NsMenuItemImage.SystemSymbol also accepts a raw symbol name string.
Add badges
NsMenuItemBadge shows a system-styled badge to the right of an item (macOS 14+). Use
Count for a plain number, Text for a custom string, or Alerts, NewItems, and
Updates for system-localized labels.
Item("Inbox", badge = NsMenuItemBadge.Count(unreadCount)) { openInbox() }
Item("Software Update", badge = NsMenuItemBadge.Updates(pendingUpdates)) { openUpdates() }What's next
- System tray — a menu-bar status item and its context menu.
- Dark mode — detect the system appearance to drive a checkable menu item.
- Freedesktop icons — the equivalent named-icon set for Linux.