Tray-anchored apps
TrayApp anchors a Compose popup window to a system tray icon so you can build menu bar apps in Kotlin.
TrayApp anchors a full Compose window to a system tray icon: left-click the icon to open the
window beside it, and click away to close it. It implements the menu bar app pattern used by
tools such as Bartender, iStat Menus, and Hidden Bar.
From 2.1.0, TrayApp lives in composenativetray-app, not in the core tray artifact.
Add the dependency
dependencies {
implementation("dev.nucleusframework:composenativetray-app:2.1.0")
}composenativetray-app pulls in composenativetray and the Nucleus Tao
window backend. TrayApp is an extension on NucleusApplicationScope — launch with
nucleusApplication { }.
Latest release: 2.1.0
Create a tray app
Pass an icon, a tooltip, a window size, and the Compose content to render in the popup:
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import dev.nucleusframework.application.nucleusApplication
import dev.nucleusframework.composenativetray.trayapp.TrayApp
fun main() = nucleusApplication {
TrayApp(
icon = Icons.Default.Dashboard,
tooltip = "Quick dashboard",
windowSize = DpSize(300.dp, 400.dp),
) {
Column(Modifier.fillMaxSize().padding(16.dp)) {
Text("Dashboard", style = MaterialTheme.typography.h6)
Spacer(Modifier.height(8.dp))
Text("CPU: 42%")
Text("RAM: 8.2 GB")
}
}
}How it works
TrayApp is a tray icon plus a transparent, undecorated, always-on-top Compose window. The
library tracks the tray icon's screen position and places the window next to it with the correct
offset for each operating system.
Click-outside-to-dismiss is wired to the OS focus-loss event, so the popup closes the way native menu bar windows do on each desktop.
Control visibility and size with state
Hold a TrayAppState to show, hide, resize, or change the dismiss mode from your own code:
import androidx.compose.material.Button
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Dashboard
import androidx.compose.foundation.layout.Column
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import dev.nucleusframework.application.nucleusApplication
import dev.nucleusframework.composenativetray.trayapp.TrayApp
import dev.nucleusframework.composenativetray.trayapp.TrayWindowDismissMode
import dev.nucleusframework.composenativetray.trayapp.rememberTrayAppState
fun main() = nucleusApplication {
val state = rememberTrayAppState(
initialWindowSize = DpSize(350.dp, 500.dp),
initiallyVisible = false,
initialDismissMode = TrayWindowDismissMode.AUTO,
)
TrayApp(
icon = Icons.Default.Dashboard,
tooltip = "Dashboard",
state = state,
) {
Column {
Text("Dashboard")
Button(onClick = { state.hide() }) { Text("Close") }
Button(onClick = { state.setWindowSize(500.dp, 600.dp) }) { Text("Resize") }
}
}
}Add a context menu
Left-click opens the popup; right-click opens a classic menu:
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Dashboard
import dev.nucleusframework.application.nucleusApplication
import dev.nucleusframework.composenativetray.trayapp.TrayApp
import kotlin.system.exitProcess
fun main() = nucleusApplication {
TrayApp(
icon = Icons.Default.Dashboard,
tooltip = "Dashboard",
menu = {
Item(label = "Settings") { openSettings() }
Divider()
Item(label = "Quit") { exitProcess(0) }
},
) {
Text("Popup content")
}
}Hide the Dock icon on macOS
A tray app usually does not need a Dock icon. On macOS, set LSUIElement = true in the app's
Info.plist. The Nucleus Gradle plugin exposes macOS.infoPlist.extraKeysRawXml for this:
nucleus {
application {
nativeDistributions {
macOS {
infoPlist {
extraKeysRawXml = """
<key>LSUIElement</key>
<true/>
""".trimIndent()
}
}
}
}
}The app then runs with no Dock entry and no menu bar — only the tray icon.
API reference
TrayApp window options
| Parameter | Default | Notes |
|---|---|---|
windowSize | DpSize(300.dp, 200.dp) | initial size |
visibleOnStart | false | show immediately |
enterTransition / exitTransition | platform default | animations |
undecorated | true | no chrome |
resizable | false | user resize |
windowsTitle | "" | window title (used on Linux and when decorated) |
horizontalOffset / verticalOffset | 0 / platform default | nudge against the tray anchor |
TrayAppState
| API | Description |
|---|---|
isVisible: StateFlow<Boolean> | current visibility |
show() / hide() / toggle() | imperative control |
setWindowSize(size) | resize on the fly |
setDismissMode(mode) | AUTO (click outside closes) or MANUAL |
onVisibilityChanged(cb) | observe transitions |
Notes
- From 2.1.0,
getTrayPosition()andTrayPositionlive incomposenativetray-app(dev.nucleusframework.composenativetray.trayapp). They need the Tao backend for screen geometry. PreferTrayAppif you only need a popup next to the icon.
What's next
- System tray — the tray icon API that
TrayAppbuilds on. - Tray menu DSL — build the right-click menu.
- Single instance — re-launching from a
.appre-shows the popup instead of starting a second process.