Nucleus
OS integration

System tray

Render a tray icon and a native context menu from Compose on macOS, Windows, and Linux.

The system tray API draws a tray icon and its native context menu from Compose on macOS, Windows, and Linux. The icon is anything Compose can draw, the menu is a Kotlin DSL, and both react to Compose state. Tray() is a single composable that hands the rendered icon and menu to the native tray API of the current OS.

Separate repository — versioned independently

System tray ships from NucleusFramework/ComposeNativeTray with its own release cycle. Its version number is independent of Nucleus — pin it separately. The artifact is dev.nucleusframework:composenativetray.

Latest release: 2.0.1

Add the dependency

dependencies {
    implementation("dev.nucleusframework:composenativetray:2.0.1")
}

Add a tray icon

Declare a Tray() composable with an icon, a tooltip, and a menu lambda:

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Favorite
import dev.nucleusframework.tray.*

Tray(
    icon = Icons.Default.Favorite,
    tooltip = "My App",
    primaryAction = { showWindow() },
) {
    Item(label = "Show window") { showWindow() }
    Divider()
    Item(label = "Quit") { exitProcess(0) }
}

How it works

Tray() is a composable. Its icon is rasterised from Compose whenever the slot's state changes, then handed to the native tray API of the current OS. The menu lambda runs inside a DSL scope that re-evaluates on recomposition, so adding, removing, or relabelling items follows the usual Compose recomposition rules.

The tray participates in your Compose state tree: it updates through recomposition, without explicit rebuild calls or separate .ico/.png/.icns files to maintain.

Each platform maps to its native tray mechanism:

Backed by NSStatusItem in the menu bar. Vector icons render at native DPI, with optional tinting via the tint parameter.

Backed by Shell_NotifyIcon. Vector input is rasterised to a 32×32 .ico per scale factor. Pass a hand-authored .ico via windowsIcon for exact control.

Uses StatusNotifierItem (KStatusNotifierItem). On stock GNOME this requires the AppIndicator extension; on Ubuntu it works out of the box.

StatusNotifierItem is native to Plasma. Hover tooltips and badges work where Plasma supports them.

Set the icon source

The icon parameter accepts an ImageVector, a Painter, or a Compose Multiplatform DrawableResource. For full control, use iconContent to draw the icon with any composable:

// ImageVector
Tray(icon = Icons.Default.Notifications, tint = Color.White, tooltip = "App") { /* … */ }

// Painter
Tray(icon = painterResource("icon.png"), tooltip = "App") { /* … */ }

// Compose Multiplatform DrawableResource
Tray(icon = Res.drawable.app_icon, tooltip = "App") { /* … */ }

// Fully custom — any composable
Tray(
    iconContent = {
        Box(Modifier.size(24.dp).background(Color.Red, CircleShape)) {
            Text("3", color = Color.White, modifier = Modifier.align(Alignment.Center))
        }
    },
    tooltip = "3 notifications",
) { /* … */ }

Use platform-specific icons

To use a native .ico on Windows and a vector elsewhere, set windowsIcon and macLinuxIcon:

Tray(
    windowsIcon = painterResource("icon.ico"),
    macLinuxIcon = Icons.Default.Notifications,
    tint = Color.White,
    tooltip = "My App",
) { /* menu */ }

Handle the primary action

primaryAction fires on left-click (macOS/Windows) or single click (Linux, desktop-dependent). Without it, the menu opens on every click.

Tray(
    icon = Icons.Default.Favorite,
    tooltip = "My App",
    primaryAction = { showWindow() },
) {
    Item(label = "Quit") { exitProcess(0) }
}

What's next

  • Tray menu DSL — items, checkables, submenus, dividers, and reactive composition.
  • Tray apps — make the tray icon open a Compose popup window.