A Tauri v2 plugin that renders a two-line label in the macOS menu bar, similar to the Stats app's Mini widget.
Each create() call adds an independent status item — run as many as you need.
Every line can have its own color, font family, weight (bold), and monospaced digits — all independently.
Real-world example: Fund01
Fund01 is a portfolio & market tracker that uses this plugin to display real-time index and P&L data in the menu bar. Clicking any item opens a rich popup with index quotes, holdings, and more.
- macOS — full native support via
NSStatusItem+attributedTitle(a system-renderedNSAttributedString). - Windows / Linux / mobile — API compiles but returns
UnsupportedPlatform.
A runnable demo lives in examples/demo. It is a minimal Tauri v2
app that drives the plugin from plain HTML/JS (no framework). The plugin is pulled in
by relative path (../../..), so it always builds against the source in this repo:
cd examples/demo
npm install
npm run tauri:dev # macOS only — uses the dev-suffixed bundle identityDev runs use a separate
dev-suffixed bundle ID / app name (src-tauri/tauri.conf.dev.json) so they never collide with release builds on macOS 26 — see Troubleshooting below.
On macOS 26, a menu-bar app can silently fail to show its status item if Control Center's "recently used apps" list still remembers an older build of the same app. macOS remembers menu-bar visibility per bundle ID and that state survives app updates, so a single run can mark the item hidden for every later run that shares the same bundle ID. This is tracked in this analysis.
If the status item does not appear after launching:
- Quit the app, then open System Settings → Menu Bar (Control Center) and toggle the app's entry off and on again.
- Make sure you are not running two builds with the same bundle ID (e.g. dev and release at the same time) — conflicting IDs keep re-hiding each other.
- If it still does not show, rebuild the release app with a brand-new bundle ID to rule out stale remembered state.
To prevent this, always use a distinct bundle ID per flavor: the demo's
npm run tauri:dev appends dev to the identity
(productName multiline-menubar-demo-new-dev,
identifier com.tauri.multiline-menubar-demo-new.dev), while
npm run tauri:build keeps the release identity, so dev and release never
share a bundle ID.
The plugin ships as two packages that must both be installed — the Rust crate (native implementation) and the JS bindings (front-end entry point):
-
Rust crate —
tauri-plugin-multiline-menubaron crates.io, added to yoursrc-tauri/Cargo.toml:cargo add tauri-plugin-multiline-menubar
-
JS bindings —
tauri-plugin-multiline-menubar-apion npm, installed in your webview front-end:npm install tauri-plugin-multiline-menubar-api
The -api suffix on the npm package is the Tauri plugin naming convention: it
marks the JavaScript API bindings that pair with the Rust crate of the same
name (e.g. tauri-plugin-fs ↔ tauri-plugin-fs-api). The JS bindings are a
thin layer that forwards your calls to the Rust side over Tauri's IPC, so
without the crate they have nothing to call — install both.
Add the plugin to your Tauri app:
// src-tauri/src/lib.rs
tauri::Builder::default()
.plugin(tauri_plugin_multiline_menubar::init())
.run(tauri::generate_context!())
.expect("error while running tauri application");Add the default capability:
{
"permissions": [
"multiline-menubar:default"
]
}The multiline-menubar:default permission set covers core rendering and
read-only queries: create, set_text, set_font_sizes, set_layout,
set_tooltip, set_visible, set_colors, set_bold, set_font_family,
set_monospaced, set_alignment, rect, is_visible, set_auto_popup.
Higher-impact commands are not included by default and must be granted explicitly when needed:
| Command | Permission | Why it's not default |
|---|---|---|
remove |
multiline-menubar:allow-remove |
Destroys a status item |
set_menu / remove_menu |
multiline-menubar:allow-set-menu / allow-remove-menu |
Injects arbitrary menu items |
set_popup_window |
multiline-menubar:allow-set-popup-window |
Repoints which window opens as popup |
open_popup / close_popup / toggle_popup |
allow-open-popup / allow-close-popup / allow-toggle-popup |
Shows/hides a window |
{
"permissions": [
"multiline-menubar:default",
"multiline-menubar:allow-remove",
"multiline-menubar:allow-set-menu",
"multiline-menubar:allow-open-popup"
]
}Breaking change (v1.2.0): earlier versions shipped all commands in the default set. If your app relied on the removed ones, add the matching
allow-*permissions above to your capability file.
import {
create,
remove,
setVisible,
setText,
setFontSizes,
setLayout,
setTooltip,
setColors,
setBold,
setFontFamily,
setMenu,
removeMenu,
onMenuSelection,
setPopupWindow,
setAutoPopup,
openPopup,
closePopup,
togglePopup,
isVisible,
rect,
EVENT_CLICK,
EVENT_READY,
EVENT_POPUP_OPEN,
EVENT_POPUP_CLOSE,
listen,
} from "tauri-plugin-multiline-menubar-api";
await create({ id: "main" });
await setText({ id: "main", top: "Sensor", bottom: "16W" });
// Customize the font size (points) for each line. Values are clamped to the
// supported range on the native side (small 5–11 pt, large 8–16 pt).
await setFontSizes({ id: "main", top: 8, bottom: 14 });
// Choose the vertical layout: 0 = small label on top / large value below
// (default), 1 = the mirror, 2 = equal lines.
await setLayout({ id: "main", layout: 1 });
// A left click on the item automatically opens the "popup" window below it.
// Force the top and/or bottom line bold, independent of the layout:
await setBold({ id: "main", top: true, bottom: false });
// Swap a line to a specific font family (macOS Font Book name); pass
// null/'' to restore the system font. Unknown names fall back silently.
await setFontFamily({ id: "main", top: null, bottom: "Helvetica" });
// Listen to the click event if you want to drive the popup yourself instead.
await listen(EVENT_CLICK, (e) => {
console.log("clicked", e.payload); // { button, x, y, width, height }
});
console.log(await isVisible({ id: "main" })); // true
await setVisible({ id: "main", visible: false }); // was: hide()You can also call the commands directly with @tauri-apps/api/core:
import { invoke } from "@tauri-apps/api/core";
await invoke("plugin:multiline-menubar|set_text", {
payload: { id: "main", top: "Sensor", bottom: "16W" },
});
// Show/hide is a single setVisible(bool) command:
await invoke("plugin:multiline-menubar|set_visible", {
payload: { id: "main", visible: true },
});Clicking the menu bar item opens a Tauri WebView window ("popup" by default) anchored directly below the item, centered on it. The window is toggled on left click and auto-hides when it loses focus — the standard menubar-app behaviour.
To use it, define a window in tauri.conf.json (the plugin ships with a
popup example window):
transparent: trueneeds"app": { "macOSPrivateApi": true }intauri.conf.json(and themacos-private-apicargo feature) for a clean frosted background on macOS.
Popup-related commands:
| Command | Description |
|---|---|
set_popup_window({ label }) |
Choose which window is the popup (default "popup"). Call before the first open. |
set_auto_popup({ enabled }) |
Toggle automatic popup on left click (default true). |
open_popup() |
Show + position the popup below the item. |
close_popup() |
Hide the popup. |
toggle_popup() |
Toggle visibility. |
The plugin intentionally mirrors the command/event conventions used by the
macOS menubar plugin family (e.g. tauri-plugin-menubar-dnd):
- It owns its own
NSStatusItem(like the tray-based menubar plugins), so you drive the icon/text through this plugin instead of Tauri's system tray. - Commands use the same naming style:
set_visible,set_tooltip,set_popup_window,open_popup/close_popup/toggle_popup. - Events are emitted on the
multiline-menubar://scheme:multiline-menubar://ready— status item created.multiline-menubar://click—{ button: "left" | "right", x, y, width, height }.multiline-menubar://popup-open/multiline-menubar://popup-close—{ window }.
v1.0.0: the API was aligned with Tauri's
TrayIcon. Historical namesgetRect → rect,destroy → remove, andshow/hidewere folded intosetVisible(bool).removeMenuis retained andsetMenu(null)detaches the menu. SeeAPI.mdfor the full reference.
The plugin uses a small Objective-C++ helper that creates an NSStatusItem
and renders both lines as a single NSAttributedString (joined with \n)
assigned to button.attributedTitle — the system-native text-field path, the
same approach used by Clash Verge Rev's tray speed label. Per-line font
family, weight, size, color and monospaced digits are applied per character
range; the item width tracks the wider line so the label stays as narrow as
possible.
- Top line: 7 pt, light weight (label).
- Bottom line: 12 pt, regular weight (value).
The font size of each line can be customized independently via setFontSizes. Values are clamped on the native side to keep both lines inside the ~22 pt tall menu bar without overlapping:
- Top label: 5–11 pt (default 7)
- Bottom value: 8–16 pt (default 12)
The weight of each line can also be overridden independently via setBold: pass top/bottom as true to force that line bold (overriding the weight layout assigns), or false to leave it to the layout.
The font family of each line can likewise be set independently via setFontFamily: pass a macOS font family name from Font Book (e.g. "Helvetica") for top/bottom, or null/'' to keep the system font. The line still resolves the weight layout/setBold asks for, using the closest face the family provides; unknown names fall back to the system font.
On click, the native helper measures the status item's on-screen rectangle and
calls back into Rust, which emits the click event and (when auto-popup is
on) positions the popup window below the item using the primary monitor's
geometry (macOS y grows upward, Tauri y grows downward, so the y axis is
flipped).
- This plugin creates its own
NSStatusItem. It does not extend Tauri's built-in system-tray / tray icon. - Text color follows
NSColor.textColor, so it adapts automatically to light / dark mode and accessibility settings. - Popup positioning assumes the status item is on the primary monitor.



{ "label": "popup", "url": "popup.html", "width": 320, "height": 400, "decorations": false, "transparent": true, "alwaysOnTop": true, "visible": false, "skipTaskbar": true }