Nativ ui
Documentation

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:

PropertyValuePurpose
decorationsfalseRemoves window frame/titlebar
transparenttrueEnables transparent background
visiblefalseHidden by default
skipTaskbartrueNo 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: true or false
  • Set shadow: false initially 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

PositionDescription
TrayLeftLeft-aligned with tray icon
TrayCenterCentered on tray icon
TrayRightRight-aligned with tray icon
TrayBottomLeftBelow tray, left-aligned
TrayBottomCenterBelow tray, centered
TrayBottomRightBelow 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: false in window config
  • Apply shadow via window.setShadow(true) after DOM is ready
  • Ensure transparent: true is set

Taskbar Entry Appearing

  • Verify skipTaskbar: true in window config
  • On some Windows versions, also set visible: false initially

Checklist

  • Window configured as frameless + transparent
  • skipTaskbar: true to hide from taskbar
  • tauri-plugin-positioner installed with system-tray feature
  • 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)