はじめに
macOS のメニューバーに常駐するユーティリティアプリ(Bartender、Stats、iStatMenus 的なやつ)を Tauri v2 で作る方法 を解説します。
筆者が実際にリリースしている HiyokoBar(メニューバー常駐型 HUD モニタリングアプリ)の実装をベースにしています。
この記事で作れるもの
- メニューバーのトレイアイコンをクリックするとウィンドウが表示される
- ウィンドウ外をクリックすると自動で閉じる(HUD 動作)
- macOS ネイティブのすりガラス(Vibrancy)UI
- グローバルショートカットでのトグル表示
- ログイン時の自動起動
1. プロジェクトのセットアップ
必要な依存関係
# Cargo.toml
[dependencies]
tauri = { version = "2", features = ["macos-private-api", "tray-icon", "image-png"] }
tauri-plugin-positioner = { version = "2", features = ["tray-icon"] }
tauri-plugin-autostart = "2.5.1"
tauri-plugin-global-shortcut = "2.3.1"
window-vibrancy = "0.7.1"
macos-private-api feature は Vibrancy(すりガラスUI)に必須です。
tray-icon feature は Position::TrayCenter 配置に必要です。
tauri.conf.json
メニューバーアプリの設定で 重要なのは5つ :
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "MyMenuBarApp",
"version": "1.0.0",
"identifier": "com.example.menubar",
"app": {
"macOSPrivateApi": true,
"windows": [
{
"title": "MyMenuBarApp",
"width": 360,
"height": 540,
"resizable": false,
"decorations": false,
"transparent": true,
"alwaysOnTop": true,
"visible": false,
"skipTaskbar": true
}
]
}
}
| 設定 | 値 | 理由 |
|---|---|---|
macOSPrivateApi |
true |
Vibrancy に必須 |
visible |
false |
起動時はウィンドウを隠す |
decorations |
false |
タイトルバーを消す |
transparent |
true |
背景を透過させる |
skipTaskbar |
true |
Dock に表示しない |
2. コアの実装(lib.rs)
ウィンドウのトグル表示
メニューバーアプリの 心臓部 です。
use tauri::Manager;
use tauri_plugin_positioner::{WindowExt, Position};
fn toggle_window(app: &tauri::AppHandle) {
if let Some(window) = app.get_webview_window("main") {
if window.is_visible().unwrap_or(false) {
let _ = window.hide();
} else {
// macOS の app-level hide からの復帰
#[cfg(target_os = "macos")]
{
let _ = app.show();
}
let _ = window.unminimize();
// ★ これが神: トレイアイコンの直下にウィンドウを配置
let _ = window.move_window(Position::TrayCenter);
let _ = window.show();
let _ = window.set_always_on_top(true);
let _ = window.set_focus();
}
}
}
Position::TrayCenter は tauri-plugin-positioner が提供する機能で、
トレイアイコンの位置を自動検出して直下にウィンドウを配置 してくれます。
Position::TrayCenter を使うには、Cargo.toml で features = ["tray-icon"] が必要です。
これを忘れると TrayCenter のバリアントが存在しないと言われます。
3. トレイアイコンの構築
setup() 内でトレイアイコンを構築します。
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_positioner::init())
// ... 他のプラグイン ...
.setup(|app| {
// トレイアイコンを構築
let _tray = tauri::tray::TrayIconBuilder::with_id("main_tray")
.icon(
tauri::image::Image::from_bytes(
include_bytes!("../icons/tray.png")
)
.expect("failed to load tray icon"),
)
.icon_as_template(true) // macOS: ダークモード対応
.tooltip("MyMenuBarApp")
.on_tray_icon_event(|tray, event| {
// ★ positioner にトレイイベントを渡す(位置計算に必要)
tauri_plugin_positioner::on_tray_event(
tray.app_handle(), &event
);
match event {
tauri::tray::TrayIconEvent::Click {
button: tauri::tray::MouseButton::Left,
button_state: tauri::tray::MouseButtonState::Up,
..
} => {
toggle_window(tray.app_handle());
}
_ => {}
}
})
.show_menu_on_left_click(false) // 左クリックでメニューを出さない
.build(app)?;
Ok(())
})
}
ハマりポイント
tauri_plugin_positioner::on_tray_event() を呼ぶ のを忘れると、
Position::TrayCenter が正しい位置を計算できず、ウィンドウが画面の左上に表示されます。
トレイアイコンの仕様
-
icon_as_template(true)を設定すると、macOS がダークモード/ライトモードに合わせて自動で色を調整してくれます - アイコン画像は 白色 + 透過背景 の PNG を用意してください
- サイズは 22x22 px が推奨です
4. フォーカスが外れたら自動隠蔽
これが メニューバーアプリらしい動作 のカギです。
.on_window_event(|window, event| match event {
// ✕ボタンで閉じない(隠すだけ)
tauri::WindowEvent::CloseRequested { api, .. } => {
if window.label() == "main" {
let _ = window.hide();
api.prevent_close();
}
}
// ★ フォーカスが外れたら自動で隠す
tauri::WindowEvent::Focused(focused) => {
if !focused && window.label() == "main" {
let _ = window.hide();
// macOS特有: アプリ自体も hide する
#[cfg(target_os = "macos")]
{
let _ = window.app_handle().hide();
}
}
}
_ => {}
})
window.hide() と app.hide() の違い
ここが macOS 特有の罠 です。
| メソッド | 効果 |
|---|---|
window.hide() |
ウィンドウだけ隠す |
app_handle().hide() |
アプリ全体 を隠す(macOS の NSApplication.hide()) |
window.hide() だけだと、macOS の Mission Control やアプリスイッチャーにアプリが残り続けます。
app.hide() を併用することで、完全に見えなくなる ユーティリティアプリの動作を実現できます。
逆に toggle_window で表示する際は app.show() を呼んで復帰させる必要があります。
5. macOS Vibrancy(すりガラスUI)
.setup(|app| {
// Vibrancy の適用
#[cfg(target_os = "macos")]
if let Some(window) = app.get_webview_window("main") {
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
let _ = apply_vibrancy(
&window,
NSVisualEffectMaterial::HudWindow,
None,
Some(18.0), // 角丸の半径
);
}
// ... トレイアイコンの構築 ...
Ok(())
})
Material の選択肢
| Material | 見た目 | おすすめ用途 |
|---|---|---|
HudWindow |
暗めのすりガラス | ダークUI のHUDアプリ |
Popover |
標準のポップオーバー風 | 通知パネル風 |
Menu |
メニュー風 | ドロップダウン風 |
Sidebar |
サイドバー風 | 設定画面 |
UnderWindowBackground |
薄い背景 | 控えめなUI |
HudWindow がメニューバーアプリには最も合います。
CSS 側の対応
Vibrancy を見せるには、フロントエンドの背景を透過させる必要があります。
body {
background: transparent;
}
.app-container {
background: rgba(0, 0, 0, 0.3); /* 半透明にして Vibrancy を透かせる */
backdrop-filter: blur(0px); /* CSS の blur は不要(OS が処理する) */
}
6. グローバルショートカット
ウィンドウが隠れていても、ショートカットキーで呼び出せるようにします。
.plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_shortcuts(["CmdOrCtrl+Shift+H"])
.unwrap_or(tauri_plugin_global_shortcut::Builder::new())
.with_handler(|app, _shortcut, event| {
if event.state == tauri_plugin_global_shortcut::ShortcutState::Pressed {
toggle_window(app);
}
})
.build(),
)
unwrap_or() でフォールバックしているのは、ショートカットが他のアプリと衝突した場合に
アプリ全体がクラッシュしないようにするためです。
7. ログイン時の自動起動
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec![]),
))
macOS では LaunchAgent を使うのが推奨です。
ユーザーの OS 設定 > ログイン項目にも自動で表示されます。
8. 完成コード(全体像)
pub fn run() {
tauri::Builder::default()
// プラグイン
.plugin(tauri_plugin_positioner::init())
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec![]),
))
.plugin(
tauri_plugin_global_shortcut::Builder::new()
.with_shortcuts(["CmdOrCtrl+Shift+H"])
.unwrap_or(tauri_plugin_global_shortcut::Builder::new())
.with_handler(|app, _shortcut, event| {
if event.state == tauri_plugin_global_shortcut::ShortcutState::Pressed {
toggle_window(app);
}
})
.build(),
)
// コマンド
.invoke_handler(tauri::generate_handler![quit_app])
// セットアップ
.setup(|app| {
// Vibrancy
#[cfg(target_os = "macos")]
if let Some(window) = app.get_webview_window("main") {
use window_vibrancy::{apply_vibrancy, NSVisualEffectMaterial};
let _ = apply_vibrancy(&window, NSVisualEffectMaterial::HudWindow, None, Some(18.0));
}
// Tray Icon
let _tray = tauri::tray::TrayIconBuilder::with_id("main_tray")
.icon(tauri::image::Image::from_bytes(include_bytes!("../icons/tray.png"))?)
.icon_as_template(true)
.on_tray_icon_event(|tray, event| {
tauri_plugin_positioner::on_tray_event(tray.app_handle(), &event);
if let tauri::tray::TrayIconEvent::Click {
button: tauri::tray::MouseButton::Left,
button_state: tauri::tray::MouseButtonState::Up, ..
} = event {
toggle_window(tray.app_handle());
}
})
.show_menu_on_left_click(false)
.build(app)?;
Ok(())
})
// ウィンドウイベント
.on_window_event(|window, event| match event {
tauri::WindowEvent::CloseRequested { api, .. } => {
if window.label() == "main" {
let _ = window.hide();
api.prevent_close();
}
}
tauri::WindowEvent::Focused(focused) => {
if !focused && window.label() == "main" {
let _ = window.hide();
#[cfg(target_os = "macos")]
{ let _ = window.app_handle().hide(); }
}
}
_ => {}
})
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
ハマりポイントまとめ
| # | 罠 | 対処 |
|---|---|---|
| 1 | Vibrancy が効かない |
macOSPrivateApi: true を忘れてないか確認 |
| 2 | TrayCenter が左上に表示される |
on_tray_event() を呼んでいるか確認 |
| 3 |
app.hide() 後に復帰しない |
toggle_window で app.show() を呼ぶ |
| 4 | ショートカット登録失敗でクラッシュ |
unwrap_or() でフォールバック |
| 5 | Dock にアイコンが出る |
skipTaskbar: true を設定 |
| 6 | CSS 背景が不透明 | body { background: transparent; } |
まとめ
Tauri v2 でメニューバーアプリを作る最小構成は:
-
tauri-plugin-positioner で
TrayCenter配置 - window-vibrancy ですりガラス UI
- on_window_event でフォーカスアウト時の自動隠蔽
- TrayIconBuilder でトレイアイコン構築
Electron と比べて バイナリサイズが圧倒的に小さく(10MB 以下)、
メモリ使用量も少ないため、常駐アプリに Tauri は最適です。
この記事のコードは HiyokoBar の実装をベースにしています
この記事は HiyokoBar の開発経験をベースに書いています。