Tray Icon
Build custom styled tray icon context menus in Tauri v2 with glassmorphism effects on Windows 11.
Overview
This playbook covers how to create a custom styled tray icon context menu in Tauri v2 on Windows 11. Instead of using the default system context menu, we'll build a frameless, transparent popup window with:
- Custom UI rendering with React/Vue
- Rounded corners and shadows
- Glassmorphism/blur effects
- Proper positioning near the tray icon
- Auto-close behavior
Apps like Discord and Figma implement similar overlays as native frameless windows with custom rendering, using WebView2 for transparency and effects.
Window Configuration
Set up the popup as a hidden, frameless window in src-tauri/tauri.conf.json:
{
"windows": [
{
"label": "tray-popup",
"width": 300,
"height": 400,
"decorations": false,
"transparent": true,
"visible": false,
"skipTaskbar": true
}
]
}Key properties:
| Property | Value | Purpose |
|---|---|---|
decorations | false | Removes window frame/titlebar |
transparent | true | Enables transparent background |
visible | false | Hidden by default |
skipTaskbar | true | No taskbar entry |
Rounded Corners
Apply border-radius in CSS on the root element for true rounded corners on transparent frameless windows.
/* styles/globals.css */
html, #root {
border-radius: 12px;
overflow: hidden;
}
.tray-popup {
background: rgba(255, 255, 255, 0.8);
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.2);
}Notes:
- Works on Windows 11 with
shadow: trueorfalse - Set
shadow: falseinitially to avoid black edges - Enable shadow via
window.set_shadow(true)after content loads - No special WebView2 config needed beyond
transparent: true
Tray Event Handling
In src-tauri/src/main.rs, create the tray and listen for clicks:
use tauri::{
tray::{TrayIconBuilder, ClickType},
Manager
};
fn main() {
tauri::Builder::default()
.plugin(tauri_plugin_positioner::init())
.setup(|app| {
let _tray = TrayIconBuilder::new()
.icon(app.default_window_icon().unwrap().clone())
.on_tray_icon_event(|tray, event| {
// Enable tray-relative positioning
tauri_plugin_positioner::on_tray_event(tray.app_handle(), &event);
if event.click_type == ClickType::Left {
let popup = tray
.app_handle()
.get_webview_window("tray-popup")
.unwrap();
if popup.is_visible().unwrap() {
popup.hide().unwrap();
} else {
popup.show().unwrap();
popup.set_focus().unwrap();
}
}
})
.build(app)?;
Ok(())
})
.run(tauri::generate_context!())
.expect("error running tauri application");
}This triggers the popup on left-click and toggles its visibility.
Positioning Near Tray
Use tauri-plugin-positioner to position the window near the tray icon.
Positioner Installation
Add to Cargo.toml:
[dependencies]
tauri-plugin-positioner = { version = "2", features = ["system-tray"] }Positioning in JavaScript
import { getCurrentWindow } from '@tauri-apps/api/window'
import { moveWindow, Position } from '@tauri-apps/plugin-positioner'
async function showTrayPopup() {
const window = getCurrentWindow()
// Position near tray icon
await moveWindow(Position.TrayCenter)
// or Position.TrayLeft, Position.TrayRight
await window.show()
await window.setFocus()
}Call moveWindow() after show() for precise placement relative to the tray icon.
Available Positions
| Position | Description |
|---|---|
TrayLeft | Left-aligned with tray icon |
TrayCenter | Centered on tray icon |
TrayRight | Right-aligned with tray icon |
TrayBottomLeft | Below tray, left-aligned |
TrayBottomCenter | Below tray, centered |
TrayBottomRight | Below tray, right-aligned |
Glassmorphism/Blur Effect
Use tauri-plugin-window-vibrancy for native acrylic/mica effects on Windows 11.
Vibrancy Installation
Add to Cargo.toml:
[dependencies]
tauri-plugin-window-vibrancy = "2"Register the plugin in main.rs:
tauri::Builder::default()
.plugin(tauri_plugin_window_vibrancy::init())
// ...Applying Vibrancy
import { getCurrentWindow } from '@tauri-apps/api/window'
async function applyVibrancy() {
const window = getCurrentWindow()
// Apply acrylic effect (Windows 11)
await window.vibrancy('acrylic')
// or 'mica', 'blur', 'tabbed'
}CSS Fallback
Combine with CSS for consistent glassmorphism:
.tray-popup {
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.2);
}
/* Dark mode */
.dark .tray-popup {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
}Note: Native acrylic may have performance issues on some systems. Test and provide CSS fallback.
Auto-Close Behavior
Close the popup when it loses focus or user clicks outside.
Listen for Blur Event
import { getCurrentWindow } from '@tauri-apps/api/window'
import { listen } from '@tauri-apps/api/event'
import { useEffect } from 'react'
export function TrayPopup() {
useEffect(() => {
const unlisten = listen('tauri://blur', async () => {
const window = getCurrentWindow()
await window.hide()
})
return () => {
unlisten.then(fn => fn())
}
}, [])
return (
<div className="tray-popup">
{/* Your menu content */}
</div>
)
}Add Shadow After Load
Apply shadow after content loads to avoid black edges:
import { getCurrentWindow } from '@tauri-apps/api/window'
async function onContentReady() {
const window = getCurrentWindow()
await window.setShadow(true)
}Complete Example
React Component
// src/TrayPopup.tsx
import { useEffect, useState } from 'react'
import { getCurrentWindow } from '@tauri-apps/api/window'
import { listen } from '@tauri-apps/api/event'
import { moveWindow, Position } from '@tauri-apps/plugin-positioner'
interface MenuItem {
label: string
icon?: React.ReactNode
onClick: () => void
danger?: boolean
}
const menuItems: MenuItem[] = [
{ label: 'Open App', onClick: () => openMainWindow() },
{ label: 'Settings', onClick: () => openSettings() },
{ label: 'Quit', onClick: () => quit(), danger: true },
]
export function TrayPopup() {
const [ready, setReady] = useState(false)
useEffect(() => {
// Setup
const setup = async () => {
const window = getCurrentWindow()
await moveWindow(Position.TrayCenter)
await window.setShadow(true)
setReady(true)
}
// Auto-close on blur
const unlisten = listen('tauri://blur', async () => {
await getCurrentWindow().hide()
})
setup()
return () => {
unlisten.then(fn => fn())
}
}, [])
if (!ready) return null
return (
<div className="tray-popup p-2 min-w-[200px]">
<div className="space-y-1">
{menuItems.map((item) => (
<button
key={item.label}
onClick={item.onClick}
className={`
w-full px-3 py-2 text-left text-sm rounded-lg
transition-colors
${item.danger
? 'text-red-500 hover:bg-red-500/10'
: 'hover:bg-white/10'
}
`}
>
{item.icon && <span className="mr-2">{item.icon}</span>}
{item.label}
</button>
))}
</div>
</div>
)
}CSS Styles
/* Tray popup container */
.tray-popup {
background: rgba(30, 30, 30, 0.9);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 12px;
color: white;
}
/* Ensure html/body have no background */
html, body {
background: transparent;
margin: 0;
padding: 0;
}
#root {
border-radius: 12px;
overflow: hidden;
}Troubleshooting
Blurry on High DPI
If the popup appears blurry on high DPI displays, ensure DPI awareness is set:
// In main.rs setup
#[cfg(target_os = "windows")]
{
use windows::Win32::UI::HiDpi::SetProcessDpiAwareness;
unsafe { SetProcessDpiAwareness(2); } // PROCESS_PER_MONITOR_DPI_AWARE
}Black Edges on Corners
- Set
shadow: falsein window config - Apply shadow via
window.setShadow(true)after DOM is ready - Ensure
transparent: trueis set
Taskbar Entry Appearing
- Verify
skipTaskbar: truein window config - On some Windows versions, also set
visible: falseinitially
Checklist
- Window configured as frameless + transparent
-
skipTaskbar: trueto hide from taskbar -
tauri-plugin-positionerinstalled withsystem-trayfeature - Tray events trigger show/hide
- Position set with
moveWindow(Position.TrayCenter) - Blur listener for auto-close
- Shadow applied after content loads
- CSS border-radius on root element
- Glassmorphism effect (native or CSS fallback)