A macOS menu bar popover: a status item and a frosted panel that hangs from it on an arrow, built on Base UI Popover, the Eased floating layer and the shared eased-mac material.
When to use
- A menu bar utility in a Tauri or Electron app: usage meters, timers, clipboard history, quick toggles.
- A web demo or landing page that shows a Mac app's tray panel and should look and move like the real thing.
- Anywhere a
Popovershould read as native macOS: system font, translucent material, an arrow pointing at its anchor.
When not to use
Installation
npx shadcn add https://eased.dev/r/mac-menu-popover.jsonThe command adds Mac menu popover, everything it builds on and Eased’s tokens to your project.
Usage
import { MacMenuItem, MacMenuPopover, MacMenuPopoverClose, MacMenuPopoverContent, MacMenuSeparator, MacStatusItem,} from "@/components/eased/mac-menu-popover/mac-menu-popover";export function UsageMenu() { return ( <MacMenuPopover> <MacStatusItem label="Tally: 62% of this session used">62%</MacStatusItem> <MacMenuPopoverContent> <p className="font-semibold">Tally</p> <p>62% of this session · resets in 1h 48m</p> <MacMenuSeparator /> <MacMenuPopoverClose render={<MacMenuItem shortcut="⌘," />}>Settings…</MacMenuPopoverClose> <MacMenuPopoverClose render={<MacMenuItem shortcut="⌘Q" />}>Quit</MacMenuPopoverClose> </MacMenuPopoverContent> </MacMenuPopover> );}Examples
Kept inside a frame
In a mock-up, keep the panel inside the fake screen rather than the viewport.
<MacMenuPopoverContent collisionBoundary={screenElement}>…</MacMenuPopoverContent>In a desktop tray window
On the desktop the native tray icon is the status item and a small borderless window is the panel, so render your content in MacMenuPanel: the same type, fills and padding, without the popover. MacMenuItem and MacMenuSeparator work inside it. In Tauri and Electron alike:
- The window draws real vibrancy and the panel uses
material="native", its default. A CSS blur can't see the desktop through a transparent window, so"css"there gives an unblurred tint; it is for web pages. - Make the page transparent with
html, body { background: transparent; }. shadcn's base layer paintsbodywithbg-background, which covers the vibrancy. - Call
useSystemAppearance()once in the window. The vibrancy follows the system appearance and Eased's dark styles follow.dark; without the hook a dark Mac gets dark glass with black text. - Size the window to the content (115px tall for the panel below) and hide it when it loses focus, as macOS does.
import { exit } from "@tauri-apps/plugin-process";import { useSystemAppearance } from "@/components/eased/lib/mac";import { MacMenuItem, MacMenuPanel, MacMenuSeparator } from "@/components/eased/mac-menu-popover/mac-menu-popover";export function TrayWindow() { useSystemAppearance(); return ( <MacMenuPanel> <p className="font-semibold">Tally</p> <p>62% of this session · resets in 1h 48m</p> <MacMenuSeparator /> <MacMenuItem shortcut="⌘,">Settings…</MacMenuItem> {/* Electron: send an IPC message that calls app.quit() instead. */} <MacMenuItem shortcut="⌘Q" onClick={() => exit(0)}>Quit</MacMenuItem> </MacMenuPanel> );}MacMenuPopover itself is for menu bars drawn in the page: a web demo, a landing page, or an app window with its own title bar. If that window draws native vibrancy, pass material="native" to MacMenuPopoverContent too.
In a Tauri app
Checked with Tauri 2.12, tauri-plugin-positioner 2.4 and tauri-plugin-process 2. Add the plugins with tauri add positioner and tauri add process, then turn on the tray features, which tauri add leaves off:
tauri = { version = "2", features = ["tray-icon", "image-png"] }tauri-plugin-positioner = { version = "2", features = ["tray-icon"] }The window, hidden until the tray icon is clicked. windowEffects only works with transparent: true; macOSPrivateApi is not needed (a no-op since 2.12.1).
{ "app": { "windows": [ { "label": "tray", "url": "/tray", "width": 320, "height": 115, "visible": false, "transparent": true, "decorations": false, "resizable": false, "alwaysOnTop": true, "skipTaskbar": true, "visibleOnAllWorkspaces": true, "windowEffects": { "effects": ["popover"], "state": "active", "radius": 12 } } ] }}A capability for the window. process:default is only for exit; a Mac HUD window gets its own.
{ "identifier": "tray", "platforms": ["macOS"], "windows": ["tray"], "permissions": [ "core:default", "core:window:allow-show", "core:window:allow-hide", "core:window:allow-set-position", "positioner:default", "process:default" ]}The tray icon opens the window under itself and hides it on blur. show_menu_on_left_click(false) replaces the deprecated menu_on_left_click; the icon is a black-on-transparent PNG that macOS tints as a template.
use tauri::{ image::Image, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, Manager, WindowEvent,};use tauri_plugin_positioner::{Position, WindowExt};#[cfg_attr(mobile, tauri::mobile_entry_point)]pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_positioner::init()) .plugin(tauri_plugin_process::init()) .setup(|app| { // A menu bar app: no Dock icon. #[cfg(target_os = "macos")] app.set_activation_policy(tauri::ActivationPolicy::Accessory); TrayIconBuilder::new() .icon(Image::from_bytes(include_bytes!("../icons/tray.png"))?) .icon_as_template(true) .show_menu_on_left_click(false) .on_tray_icon_event(|tray, event| { tauri_plugin_positioner::on_tray_event(tray.app_handle(), &event); if let TrayIconEvent::Click { button: MouseButton::Left, button_state: MouseButtonState::Up, .. } = event { let Some(win) = tray.app_handle().get_webview_window("tray") else { return }; if win.is_visible().unwrap_or(false) { let _ = win.hide(); } else { let _ = win.move_window(Position::TrayBottomCenter); let _ = win.show(); let _ = win.set_focus(); } } }) .build(app)?; Ok(()) }) // Hide the panel when it loses focus, as macOS does. .on_window_event(|window, event| { if window.label() == "tray" && matches!(event, WindowEvent::Focused(false)) { let _ = window.hide(); } }) .run(tauri::generate_context!()) .expect("error while running tauri application");}In an Electron app
The main process makes the tray icon and a hidden vibrancy window, and places the window under the icon on each click. tray.getBounds() can fall off every screen (an icon hidden behind the notch, or a second display), so the position is clamped to the nearest display.
const path = require("node:path");const { app, BrowserWindow, Tray, nativeImage, screen } = require("electron");let tray, panel;app.whenReady().then(() => { app.dock?.hide(); // A menu bar app: no Dock icon. panel = new BrowserWindow({ width: 320, height: 115, show: false, frame: false, transparent: true, backgroundColor: "#00000000", resizable: false, skipTaskbar: true, alwaysOnTop: true, vibrancy: "popover", visualEffectState: "active", }); panel.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true }); panel.on("blur", () => panel.hide()); panel.loadFile(path.join(__dirname, "../dist/index.html"), { query: { view: "tray" } }); const icon = nativeImage.createFromPath(path.join(__dirname, "trayTemplate.png")); icon.setTemplateImage(true); tray = new Tray(icon); tray.on("click", () => { if (panel.isVisible()) return panel.hide(); const t = tray.getBounds(); const { workArea } = screen.getDisplayMatching(t); const { width, height } = panel.getBounds(); const x = Math.min(Math.max(t.x + t.width / 2 - width / 2, workArea.x + 8), workArea.x + workArea.width - width - 8); const y = Math.min(Math.max(t.y + t.height + 4, workArea.y), workArea.y + workArea.height - height); panel.setPosition(Math.round(x), Math.round(y)); panel.show(); panel.focus(); });});macOS rounds the frameless window's corners (about 10pt) and draws its shadow; Electron can't round the vibrancy further.
API reference
MacMenuPopover
Base UI Popover.Root: open, defaultOpen, onOpenChange, modal (non-modal by default), actionsRef.
MacStatusItem
Base UI Popover.Trigger props, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | Required accessible name, e.g. "Usage: 62% of this session used". | |
className | string | "" | Appended to the item's classes. |
A 22px menu bar button. Children are its icon and optional short value.
MacMenuPopoverContent
| Prop | Type | Default | Description |
|---|---|---|---|
width | number | 320 | Panel width, px; capped at the available width. |
arrow | boolean | true | Draw the arrow. Without it the panel is a plain rounded rectangle. |
material | "css" | "native" | "css" | native drops the CSS blur and tint for a window that draws its own vibrancy. |
side | "bottom" | "top" | "bottom" | Flips on collision; the arrow moves to the facing edge. |
align | "start" | "center" | "end" | "center" | |
sideOffset | number | 6 | Gap from the status item to the arrow tip, px. |
alignOffset | number | 0 | |
collisionPadding | number | 8 | Space kept from the boundary, px. |
collisionBoundary | Element | Element[] | Rect | "clipping-ancestors" | viewport | The area the panel stays inside. |
initialFocus | Base UI | see description | Where focus goes on open. Default: the first control when opened from the keyboard, the panel itself when clicked, so no row lights up. |
finalFocus | Base UI | the status item | Where focus returns on close. |
container | HTMLElement | null | document.body | Portal target. |
zIndex | number | 50 | |
className | string | "" | Appended to the content area (a flex column with 12px padding). |
The panel exposes --pad (12px) and --radius-outer (11px) for concentric children.
MacMenuPanel
The panel's material, type, fills and 12px padding as a plain div, for a native tray window. material ("native"), className, children. It fills the window, so the window draws the corners.
MacMenuItem
A <button> row. All button props, plus icon (a 16px icon on the content line) and shortcut (display text such as "⌘D"; wire the key yourself). Wrap it in MacMenuPopoverClose with render when the action should close the panel.
MacMenuSeparator, MacMenuPopoverClose
A divider on the content lines (className), and Base UI Popover.Close.
useSystemAppearance
From @/components/eased/lib/mac. Call it once in a Tauri or Electron window: it puts .dark on <html> while prefers-color-scheme is dark and follows changes, so the text matches the native vibrancy. It runs in an effect, so it is safe with server rendering. Eased otherwise follows .dark or data-theme="dark", so leave it out of a web app with its own theme toggle.
Keyboard
| Key | Action |
|---|---|
Enter / Space on the status item | Opens or closes the panel. |
Tab / Shift+Tab | Moves through the panel's controls; focus stays in the page. |
Esc | Closes the panel and returns focus to the status item. |
Accessibility
- The status item is a button named by
label, witharia-expandedwhile open. Its visible text can be just a number; the label says what the number means. - The panel is a Base UI popover (
role="dialog", non-modal). A keyboard open moves focus to its first control (a click focuses the panel itself); focus returns to the status item on close; Escape and an outside click dismiss it. - Rows are real buttons. Hover and keyboard focus draw the same highlight, so focus is never invisible. The shortcut text is display only.
- The status item is 22px tall, as in the macOS menu bar; it sits in a dense row of targets, so it isn't enlarged to 44px.
Motion
The panel grows from the arrow tip (Base UI's --transform-origin, which tracks the arrow) using the floating layer's transitions: it drops 6px toward the menu bar and scales from 0.95 on the snappy spring while fading in over 200ms, then leaves at half the distance and scale in 150ms. Open and close are CSS transitions, so a close during the open turns around mid-flight. The shadow layer copies the panel's motion state, so it moves in step. The status item presses and stays highlighted by colour only, never by scale. Reduced motion keeps a 100ms fade.
Notes for AI
Pick this for anything that should look like a Mac menu bar extra. In a Tauri or Electron tray window render MacMenuPanel (native material by default) in a window with native vibrancy, a transparent html, body, and useSystemAppearance(). Compose MacMenuPopover › MacStatusItem + MacMenuPopoverContent, fill the content with your own layout, and use MacMenuItem / MacMenuSeparator for action rows. Always pass label to MacStatusItem.
Don't use material="css" in a desktop window: the blur can't see the desktop. Don't wrap the content in another card, border, shadow or blur: the panel draws its own material, hairline and shadow as one shape. Don't put a filter, opacity or backdrop-filter on an ancestor of the panel; it stops the blur from seeing the page. Don't scale the status item on press, and don't add animation to the panel: it already uses the shared floating motion.