SolidStart and TanStack Start with deno desktop
SolidStart and TanStack Start both use the Nitro framework underneath. deno desktop detection handles them via the .output/server/index.* entry. Users must build first with `npm run build` before running `deno desktop`.
menuclick event for application menu
Application menu clicks fire a 'menuclick' event. Access the clicked item's id via e.detail.id. Items without an id and role items (handled by the OS) do not produce menuclick events.
CmdOrCtrl is preferred for cross-platform shortcuts
Use CmdOrCtrl instead of Cmd or Ctrl directly for common shortcuts so you do not need to branch on platform.
Role items for standard OS menu commands
A role item { role: { role: string } } maps to a standard OS menu command. The platform provides the label, accelerator, and behavior. Common roles include quit, undo, redo, cut, copy, paste, selectAll, minimize, and close. Role items need no id and never fire menuclick events; the OS handles them directly.
macOS menu bar first submenu becomes application menu
On macOS, the first top-level submenu becomes the application menu (with the app's name), and its label is replaced with the app name. Put standard app roles like quit there. If you do not provide one, a default is generated.
macOS Edit menu standard items work natively with role items
On macOS, the Edit menu's standard items (Cut, Copy, Paste, Select All, Undo, Redo) work natively when included as role items.
showContextMenu API for right-click menus
Call win.showContextMenu(x, y, menuItems) to show a context menu at a screen position. The menu uses the same MenuItem type as application menus.
contextmenuclick event for context menu clicks
Context menu clicks fire a 'contextmenuclick' event with e.detail.id containing the clicked item's id. Context-menu clicks are separate from application-menu clicks (menuclick), so you do not need to namespace ids to tell them apart.
Dynamic menu updates via setApplicationMenu
The application menu can be replaced at any time by calling setApplicationMenu again. There is no 'update single item' API; rebuild the entire array and call setApplicationMenu when state changes. For frequently-updated menus, batch updates rather than calling on every change.
Disable menu items with enabled: false
Set enabled: false on an item to gray it out. There is no visible flag; to hide an item, exclude it from the array and call setApplicationMenu again.
deno desktop available in Deno 2.9+
deno desktop is available starting in Deno v2.9.0. If you are on an earlier version, update Deno to use it.
Application menu example with File and Edit menus
Example showing setApplicationMenu with two top-level submenus. File menu has New (id: 'new'), Open (id: 'open'), separator, Save (id: 'save'), and quit role. Edit menu has undo, redo, separator, cut, copy, paste roles. All items have accelerators like 'CmdOrCtrl+N'. Accelerators use keyboard shortcuts; enabled: true makes items active. The menuclick event listener switches on e.detail.id to handle clicks.
Context menu example with mousedown handler
Example showing a context menu triggered on right-click (e.button === 2). The menu contains Copy (id: 'copy'), Paste (id: 'paste'), separator, and Properties (id: 'props') items. win.showContextMenu(e.clientX, e.clientY, contextMenu) displays it at mouse position. contextmenuclick event listener checks e.detail.id to handle the clicked item.
Dynamic menu example with conditional enabled state
Example showing rebuildEditMenu(canUndo) that replaces the entire application menu by calling setApplicationMenu with an Edit submenu containing a single Undo item. The item's enabled property is set to the canUndo parameter, demonstrating how to update menu state by rebuilding and re-calling setApplicationMenu.
MenuItem type definition - all four shapes
MenuItem is a tagged union with four possible shapes: (1) A clickable item with label (required), id (optional, returned in click event), accelerator (optional, e.g. 'CmdOrCtrl+S'), and enabled (required boolean). (2) A submenu with label and items array containing more MenuItems. (3) The string 'separator' for a divider line. (4) A role object with { role: { role: string } } for standard OS commands.
setApplicationMenu API for window menus
Call win.setApplicationMenu(menuItems) to set the menu shown in the macOS menu bar or the Windows/Linux window menu. The menu is an array of MenuItem objects. Pass an empty array or call again to replace the menu.
Accelerator syntax and modifiers
Accelerators use the format Modifier+Modifier+Key. Modifiers are: Cmd (macOS only), Ctrl (all platforms), CmdOrCtrl (Cmd on macOS, Ctrl elsewhere), Alt (all platforms, Option on macOS keyboards), Shift (all platforms), Super (Windows/Meta key). Keys are letters A-Z, numbers 0-9, function keys F1-F24, or named keys: Enter, Esc, Up, Down, Left, Right, Tab, Space, Backspace, Delete.
macOS requires code-signed bundle for notifications
macOS only grants notification permission to an app with a stable code identity. deno desktop ad-hoc-signs every bundle it produces and re-signs the cached runtime in --hmr mode, so notifications work out of the box. For real signing identity configuration, see the Distribution documentation.
Dismissing notifications with close()
Call close() to dismiss a notification programmatically. Notifications are otherwise fire-and-forget. The OS owns them once shown, and close() is a best-effort request.
Permissions API for live OS notification state
Query the Permissions API to get the live OS state of notification permissions, including whether the platform has a permission model at all: const status = await navigator.permissions.query({ name: "notifications" }); console.log(status.state); // "granted" | "denied" | "prompt". On platforms with no permission concept (unbundled macOS process, some Linux notification daemons), the query reports "prompt" and notifications are shown without an explicit grant.
Notification permission check and request example
if (Notification.permission !== "granted") {
const permission = await Notification.requestPermission();
if (permission !== "granted") {
return;
}
}
new Notification("All set", { body: "Notifications are enabled." });
Notification.permission states and requestPermission()
Notification.permission is a cached synchronous getter that holds the result of the most recent permission query or request. It returns 'granted', 'denied', or 'default'. The method Notification.requestPermission() is async and triggers a system prompt the first time the user has not yet decided, resolving to 'granted', 'denied', or 'default'. Check permission state before showing notifications.
Notification event listener example
const n = new Notification("Download finished");
n.onshow = () => console.log("shown");
n.onclick = () => openDownloadsFolder();
n.onclose = () => console.log("dismissed");
n.onerror = () => console.warn("the OS rejected the notification");
Notification constructor basic usage
const n = new Notification("Build complete", {
body: "Your binary is ready.",
});
n.addEventListener("click", () => win.focus());
Notification API available in deno desktop
The Web Notifications API is implemented in deno desktop as the standard Notification constructor. It shows native OS notifications: macOS User Notifications, Windows toast notifications, or Linux desktop notification service. Notification is only defined in apps compiled with deno desktop; in a plain deno run script it does not exist.
Notification events table
A Notification is an EventTarget with the following events:
| Event | When it fires |
| --- | --- |
| show | The OS displayed the notification. |
| click | The user clicked the notification body. |
| close | The user dismissed it, or it expired. |
| error | The OS could not display it (e.g. permission denied). |
Listen with addEventListener or on<event> properties.
Using file-based icons with Notification
import { encodeBase64 } from "jsr:@std/encoding/base64";
const bytes = await Deno.readFile("./icons/alert.png");
const dataUrl = "data:image/png;base64," + encodeBase64(bytes);
new Notification("Heads up", { icon: dataUrl });
Notification icon support in desktop runtime
The desktop runtime can only resolve data: URLs synchronously. An inline data:image/png;base64,… icon is rendered. Other URL schemes (https:, file:) are accepted and round-trip through the icon property, but the OS notification is shown without an icon.
NotificationOptions table
The Notification constructor takes a NotificationOptions object with these fields:
| Option | Type | Notes |
| --- | --- | --- |
| body | string | The notification's body text. |
| icon | string | Icon URL. Only data: URLs are shown. |
| tag | string | Replaces any existing notification with the same tag instead of stacking. |
| requireInteraction | boolean | Keep the notification visible until the user dismisses it. |
| silent | boolean \| null | Suppress the notification sound. |
| badge | string | Badge URL (platform-dependent). |
| dir | "auto" \| "ltr" \| "rtl" | Text direction. |
| lang | string | BCP 47 language tag. |
| data | any | Arbitrary data attached to the notification; read it back from data. |
Cannot override port in deno desktop
You cannot override the port that Deno.serve() binds to inside deno desktop. This is intentional because the webview needs to navigate to the same port the runtime is listening on, and the runtime is the source of truth for that value.
Deno.serve() in deno desktop - automatic port binding
When a deno desktop app starts, the runtime picks an unused local port and sets the DENO_SERVE_ADDRESS environment variable to tcp:127.0.0.1:<port>. When Deno.serve() is called, it reads DENO_SERVE_ADDRESS and binds to that port, ignoring any port passed as an argument. The webview navigates to http://127.0.0.1:<port> once the listener is ready.
deno desktop serves UI over local HTTP
A deno desktop app keeps the same structure as a normal Deno website by serving its UI over local HTTP and pointing an embedded webview at it. Deno.serve() is the entry point and every request flows through the handler with no port to manage and no remote network exposure.
Network binding in deno desktop is always local
The bound address in deno desktop is always 127.0.0.1 (or [::1]). The compiled binary never binds to a public interface, even if 0.0.0.0 is passed to Deno.serve(). Other apps and other users on the same machine cannot reach the server.
Reading DENO_SERVE_ADDRESS for custom logic
To find out where the server is bound in deno desktop, read the DENO_SERVE_ADDRESS environment variable, which is in tcp:127.0.0.1:<port> form. Split off the port when you need an http:// URL.
Multiple windows in deno desktop use same HTTP server
When you create additional windows in deno desktop, they all load from the same local HTTP server by default. Use different paths per window to differentiate, such as http://127.0.0.1:<port>/settings.
deno desktop available in Deno 2.9
The deno desktop command is available starting in Deno v2.9.0.
deno desktop example with Deno.serve()
Here is an example deno desktop app:
```ts
Deno.serve((req) => {
const url = new URL(req.url);
if (url.pathname === "/api/hello") {
return Response.json({ hello: "world" });
}
return new Response(HOMEPAGE, {
headers: { "content-type": "text/html" },
});
});
const HOMEPAGE = `<!doctype html>
<html><body>
<h1>Hello, desktop</h1>
<button onclick="fetch('/api/hello').then(r => r.json()).then(console.log)">
Ping
</button>
</body></html>`;
```
Run with: `deno desktop main.ts`
deno desktop example with default export
A deno desktop app can also use the default-export form:
```ts
export default {
fetch(req: Request): Response {
return new Response("Hello!");
},
};
```
Reading DENO_SERVE_ADDRESS example
To read the server address in deno desktop and log it:
```ts
const addr = Deno.env.get("DENO_SERVE_ADDRESS")!; // "tcp:127.0.0.1:54321"
const port = addr.split(":").pop();
console.log("Serving on:", `http://127.0.0.1:${port}`);
```
Multiple windows example in deno desktop
To create additional windows in deno desktop that load from different paths:
```ts
const port = Deno.env.get("DENO_SERVE_ADDRESS")!.split(":").pop();
const settings = new Deno.BrowserWindow();
settings.navigate(`http://127.0.0.1:${port}/settings`);
```
Tray-only background app pattern
To run as a status-bar-only background process with no dock and no main window: call Deno.dock.setVisible(false) to hide from the dock (macOS), hide the implicit startup window with win.hide(), create a tray with icon, tooltip, and menu, and respond to menuclick events. The startup window is created when the binary launches; hiding it keeps it ready to be shown without a startup delay later.
Tray tooltip
Use tray.setTooltip("text") to set a tooltip on the tray icon. Call tray.setTooltip(null) to remove the tooltip.
Deno.Tray available in Deno 2.9
Deno.Tray is available starting in Deno v2.9.0. If you are on an earlier version, you need to update Deno to use it.
Deno.Tray puts icon in system status area
Deno.Tray puts an icon in the system status area: macOS menu bar extras, Windows system tray, or Linux AppIndicator.
Deno.dock controls app dock and taskbar
Deno.dock is a singleton that controls the app's dock or taskbar presence, including badge, bounce, visibility, and a custom menu. It is available on all platforms but macOS-only operations are no-ops on Windows and Linux.
Tray lifecycle and destruction
A tray icon stays in the status area until you call tray.destroy() or the process exits. Multiple trays can coexist. Tray is also a Disposable, so it works with the using statement for automatic destruction at scope exit.
Setting tray icon with dark mode variant
Use tray.setIcon(pngBytes) to set the tray icon with PNG-encoded bytes, not a file path. Use tray.setIconDark(darkPngBytes) to provide a separate dark-mode variant. Call tray.setIconDark(null) to clear the dark icon. For best results, use a template image style (mostly opaque silhouette, transparent elsewhere) at 22×22 logical pixels for macOS or 16×16 for Windows.
Tray context menu with menuclick event
Right-click on a tray icon opens a menu set by tray.setMenu(). The menu items use the same Deno.MenuItem shape as application menus, including separators and submenus. Listen to the menuclick event on the tray to respond to menu selections: tray.addEventListener("menuclick", (e) => { ... }). The event detail includes the item id. Call tray.setMenu(null) to remove the menu without destroying the tray.
Tray click and double-click events
Listen for click and dblclick events on a tray: tray.addEventListener("click", ...) and tray.addEventListener("dblclick", ...). The click event fires on primary-button click; dblclick fires on double-click. Right-click is reserved for the context menu on all platforms, so only left-click produces these events.
Tray popover panel with attachPanel
Use tray.attachPanel({url, width, height, hideOnBlur, position}) to attach a small floating window anchored under the tray icon. The returned Deno.TrayPanel toggles on tray click, hides when it loses focus (default hideOnBlur is true), and supports a position callback to override placement. Call panel.show(), panel.hide(), panel.toggle(), check panel.visible, or call panel.destroy(). Access the underlying BrowserWindow via panel.window.
Tray.getBounds returns icon screen rectangle
Call tray.getBounds() to get the icon's screen rectangle as {x, y, width, height} or null when the platform cannot report it. On Linux, the icon position cannot be queried, so an attached panel shows at its last position rather than anchored to the icon.
Tray platform support requirements
macOS: status menu items (NSStatusItem). Windows: system tray (NotifyIcon). Linux: AppIndicator / KStatusNotifierItem, requires a desktop environment that surfaces them; most do, but some minimal i3 setups need extras like swaync or polybar configuration. If the backend cannot create a tray icon, the constructor's underlying trayId is 0 and subsequent calls are no-ops. Check tray.trayId !== 0 to fall back gracefully.
Deno.dock.setBadge sets text badge
Use Deno.dock.setBadge("text") to set a text badge on the dock icon (macOS) or taskbar icon (Windows); on Linux it prefixes the focused window's title. Call Deno.dock.setBadge(null) or Deno.dock.setBadge("") to clear the badge. Badges are short, typically a count; the OS truncates long strings.
Deno.dock.bounce bounces or flashes dock
Use Deno.dock.bounce() to bounce the dock icon once (macOS), flash the taskbar button once (Windows), or set the urgency hint on the focused window (Linux). Use Deno.dock.bounce(true) to bounce continuously until the app is focused. The optional critical argument defaults to false.
Deno.dock.setVisible hides or shows dock (macOS)
Use Deno.dock.setVisible(false) to hide the app from the dock and Cmd-Tab switcher (macOS only). Use Deno.dock.setVisible(true) to restore it. This is useful for menu-bar-only apps. The application keeps running and can still show windows; users can reach it via Spotlight or the tray icon. This is a no-op on Windows and Linux.
Deno.dock.setMenu sets dock context menu (macOS)
Use Deno.dock.setMenu([...]) to set a custom right-click menu on the dock icon (macOS only). The menu items use the same Deno.MenuItem shape as tray menus. Listen to menuclick events on Deno.dock to respond to menu selections. Call Deno.dock.setMenu(null) to remove the menu.
Deno.dock reopen event (macOS)
On macOS, clicking the dock icon while the app has no visible windows fires a reopen event on Deno.dock. The default "show the last hidden window" behavior is swallowed, so you decide what to do. Listen with Deno.dock.addEventListener("reopen", (e) => { if (!e.detail.hasVisibleWindows) ... }).
Surface sizing and resizing for WebGPU
Set surface.width and surface.height before the first frame, and update them (and let the context reconfigure) whenever the window's resize event fires. A surface that does not match the window is stretched or clipped.
Raw backend for native window rendering with WebGPU
The raw backend provides a native window with no web engine attached. Instead of loading HTML, you draw to the window yourself using WebGPU. This backend is appropriate for games, visualizations, emulators, and any app that renders its own pixels rather than a document.
WebGPU setup requires unstable flag and raw backend configuration
WebGPU is behind an unstable flag. The raw backend is selected through the deno.json configuration file with the field "desktop": { "backend": "raw" } and "unstable": ["webgpu"]. The raw backend cannot be passed with --backend on the command line and is only selectable through the desktop.backend field in deno.json.