VOOZH about

URL: https://dev.to/hiyoyok/building-a-mini-dashboard-widget-in-tauri-the-menubar-mini-dash-pattern-4m6h

⇱ Building a Mini Dashboard Widget in Tauri — The Menubar Mini-Dash Pattern - DEV Community


All tests run on an 8-year-old MacBook Air. All results from shipping 7 Mac apps as a solo developer. No sponsored opinion.

HiyokoAutoSync has a Mini-Dash — a compact menubar widget showing sync status, device connection, and quick actions. It lives separately from the main window and is always one click away. Here's the pattern.


Why a Mini-Dash

The main app window has full controls. The mini-dash has just the essentials:

  • Is the device connected?
  • Is sync running?
  • Last sync time
  • One-click sync trigger

Users who set up sync and mostly let it run don't want to open the full app every time. The mini-dash is for them. It's the difference between a tool that stays out of your way and one that doesn't.


Two-Window Tauri Setup

tauri::Builder::default()
 .setup(|app| {
 // Main window
 let _main = WebviewWindowBuilder::new(
 app,
 "main",
 WebviewUrl::App("index.html".into())
 )
 .title("HiyokoAutoSync")
 .inner_size(800.0, 600.0)
 .visible(false)
 .build()?;

 // Mini-dash window
 let _mini = WebviewWindowBuilder::new(
 app,
 "mini",
 WebviewUrl::App("mini.html".into())
 )
 .title("HiyokoAutoSync Mini")
 .inner_size(320.0, 200.0)
 .always_on_top(true)
 .decorations(false)
 .resizable(false)
 .visible(false)
 .build()?;

 Ok(())
 })

Two separate windows, separate HTML entry points, same Rust backend. The key settings for the mini-dash:

Setting Value Reason
always_on_top true stays visible over other windows
decorations false no titlebar — pure content
resizable false fixed compact size
visible false hidden until tray click

Positioning the Mini-Dash

A mini-dash that appears at a random position feels broken. Use tauri-plugin-positioner to snap it below the tray icon:

# Cargo.toml
tauri-plugin-positioner = { version = "2", features = ["tray-icon"] }
fn toggle_mini_dash(app: &tauri::AppHandle) {
 if let Some(window) = app.get_webview_window("mini") {
 if window.is_visible().unwrap_or(false) {
 let _ = window.hide();
 } else {
 let _ = window.move_window(Position::TrayCenter);
 let _ = window.show();
 let _ = window.set_focus();
 }
 }
}

Without this, always_on_top windows tend to appear wherever they were last positioned — which is rarely where the user expects.


Shared State Between Windows

Both windows talk to the same Rust backend. Broadcast events to all windows at once:

// Broadcast to all windows
app_handle.emit("sync-status-changed", &status).ok();

// Or target a specific window
app_handle
 .get_webview_window("mini")
 .unwrap()
 .emit("sync-status-changed", &status)
 .ok();

The mini-dash listens for status events and updates its compact UI. The main window does the same with its full UI. One event, two listeners — no duplication.


Compact UI Design

The mini-dash is 320x200px. Every element earns its space:

export function MiniDash() {
 const [status, setStatus] = useState<SyncStatus>()

 useEffect(() => {
 listen<SyncStatus>('sync-status-changed', (e) => {
 setStatus(e.payload)
 })
 }, [])

 return (
 <div className="p-3 space-y-2">
 <DeviceIndicator connected={status?.deviceConnected} />
 <SyncStatusLine status={status?.syncState} />
 <LastSyncTime time={status?.lastSync} />
 <button onClick={() => invoke('trigger_sync')}>
 Sync Now
 </button>
 </div>
 )
}

Four elements. That's the whole UI. Resist the urge to add more — the value of a mini-dash is what it leaves out.


Tray Icon Integration

Left click → mini-dash. Right click → context menu with full options.

TrayIconBuilder::new()
 .on_tray_icon_event(|tray, event| {
 // Pass event to positioner first
 tauri_plugin_positioner::on_tray_event(tray.app_handle(), &event);

 match event {
 TrayIconEvent::Click {
 button: MouseButton::Left,
 button_state: MouseButtonState::Up,
 ..
 } => {
 toggle_mini_dash(tray.app_handle());
 }
 _ => {}
 }
 })
 .menu(&menu) // Right-click: "Open Main Window", "Quit"
 .show_menu_on_left_click(false)
 .build(app)?;

NOTE: tauri_plugin_positioner::on_tray_event() を呼ぶのを忘れると、Position::TrayCenter が正しく動きません。


If this was useful, a ❤️ helps more than you'd think!
HiyokoAutoSync → https://hiyokomtp.lemonsqueezy.com/checkout/buy/20c922f1-ca45-4f77-aeb2-04e34aad2fb4

X → @hiyoyok