A practical tutorial on Deno 2.9's new deno desktop command — from a
hello world desktop app to framework integration, native APIs, and deployment.
With a comparison against Electron and Tauri.
Building a desktop application with web technologies has traditionally meant one thing: Electron. It works, but the trade-offs are well-known — 150+ MB bundles, a full Chromium instance per window, and a Node.js process that burns through memory. Alternatives like Tauri (Rust-based, smaller bundles) require a separate toolchain and a different programming language for the backend.
Deno 2.9, released June 25, 2026, introduces deno desktop — a new subcommand
that turns JavaScript and TypeScript projects into native desktop applications on macOS,
Windows, and Linux. Unlike Electron, you can choose between the OS-native WebView or a
bundled Chromium (CEF) backend. Unlike Tauri, you write everything in TypeScript with
zero extra tooling.
In this article, I'll walk through building a real desktop app from scratch — covering
setup, the WebView vs CEF choice, native APIs like tray icons and browser windows,
framework auto-detection, cross-compilation, and the --compress option
that squeezes bundle size from 65MB to 19MB.
First, make sure you have Deno 2.9 installed. Upgrade if you're on an older version:
deno upgrade
The simplest possible Deno desktop app is a script that calls Deno.serve() —
the desktop runtime automatically binds the server to the port the WebView opens.
Create a file called main.ts:
Deno.serve(() =>
new Response(
`<!DOCTYPE html><h1>Hello from Deno desktop 👋</h1>`,
{ headers: { "content-type": "text/html" } },
)
);
Now run it with:
deno desktop main.ts
That's it. A native window opens, rendering your HTML page. No package.json,
no build step, no Electron boilerplate. The Deno.serve() call inside a desktop
entrypoint automatically detects the desktop environment and binds to the correct port.
During development, pass the --hmr flag for instant reload on file changes:
deno desktop --hmr main.ts
The WebView refreshes automatically when you save your code — no manual restart required. HMR preserves application state across reloads, making UI iteration faster.
Deno desktop gives you two rendering backends — this is one of its biggest differentiators from Electron, which always bundles the full Chromium renderer.
| Feature | WebView (Default) | CEF (Chromium Embedded) |
|---|---|---|
| Bundle Overhead | ~1 MB | ~45 MB added |
| Rendering Engine | OS-native (WKWebView / WebView2 / WebKitGTK) | Bundled Chromium |
| Consistency | Depends on OS browser version | Identical rendering everywhere |
| Total App Size (compressed) | ~19 MB | ~65+ MB |
| Best For | Internal tools, utility apps | Consumer apps, pixel-perfect UI |
By default, Deno desktop uses the OS-native WebView. To switch to CEF, add a
configuration section to your deno.json:
{
"desktop": {
"backend": "cef"
}
}
The choice depends on your audience. For an internal admin tool used by a team on modern machines, the WebView backend is faster to distribute and lighter. For a consumer-facing app where consistent rendering across Windows 10, Windows 11, and macOS is critical, the Chromium backend eliminates browser-compatibility surprises.
One of the most practical features of deno desktop is its ability to
auto-detect popular web frameworks. Run it with no entrypoint in a project directory,
and it identifies the framework, builds it, and wraps the result in a desktop window.
cd my-nextjs-app
deno desktop # auto-detect, build, and launch
The following frameworks are supported:
With deno desktop, a Next.js dashboard that normally takes 30 minutes to
Dockerize and deploy as a web app becomes a single command to wrap into a distributable
binary. This is especially useful for internal tools, prototyping, and client demos.
Beyond rendering HTML, Deno desktop exposes a full set of native APIs directly on the
Deno.* namespace — no npm packages required.
Programmatic control over window size, position, visibility, and DevTools:
const win = new Deno.BrowserWindow({
width: 1024,
height: 768,
title: "My App",
resizable: true,
devTools: Deno.env.get("DEV") ? true : false,
});
win.setPosition({ x: 100, y: 50 });
win.setMenuBar(false);
console.log(`Window ID: ${win.id}`);
You can also bridge between the WebView and Deno — bind a function in your entrypoint:
// In your Deno desktop entrypoint
window.bind("readFile", async (path: string) => {
return await Deno.readTextFile(path);
});
Then call it from your page JavaScript via the bindings namespace:
<script>
const content = await bindings.readFile("/path/to/file.txt");
document.getElementById("content").textContent = content;
</script>
This bridge pattern is significantly simpler than Electron's IPC — no preload scripts, no contextBridge boilerplate, no message channels.
Add a system tray icon with an attached panel for quick actions:
const tray = new Deno.Tray();
const iconBytes = await Deno.readFile("./icon.png");
tray.setIcon(iconBytes);
const panel = tray.attachPanel({
url: "http://localhost:8000/panel",
});
panel.window.bind("doThing", async () => {
// Handle tray action
console.log("Tray action triggered");
});
macOS dock integration for managing the dock icon and recent items:
if (Deno.build.os === "darwin") {
Deno.Dock.setBadge("3"); // Show badge count
}
The standard prompt(), alert(), and confirm()
calls in your page JavaScript render as native OS dialogs — not browser-styled modals.
This gives a professional desktop feel with zero extra code.
Deno.autoUpdate() wires up a polling auto-updater that checks for new
releases and applies binary patches in the background:
const updater = Deno.autoUpdate({
interval: 1000 * 60 * 60 * 6, // every 6 hours
endpoint: "https://updates.example.com/check",
});
updater.on("update-available", (info) => {
console.log(`New version: ${info.version}`);
updater.apply();
});
Deno desktop supports cross-compilation — build for macOS from Linux, or for Windows
from macOS, all from the same codebase. This is built on top of Deno's existing
deno compile cross-compilation infrastructure introduced in 2.8:
# Build a compressed desktop app for all three platforms
deno desktop main.ts --target x86_64-unknown-linux-gnu
deno desktop main.ts --target x86_64-apple-darwin
deno desktop main.ts --target x86_64-pc-windows-msvc
# With compression
deno desktop main.ts --compress --target x86_64-apple-darwin
This makes CI/CD pipelines simple — one CI job can produce installers for all platforms without needing separate build agents per OS.
As noted in JavaScript Weekly #791, the --compress option is not documented
in the official blog post but reduces package sizes dramatically. In tests with a basic
app, compression brings the binary from ~65MB down to ~19MB
deno desktop main.ts --compress
The compression works by stripping debug symbols, minifying embedded assets, and applying binary compression to the runtime. This is one area where Deno desktop significantly narrows the gap with Tauri (which achieves 3-5MB bundles) while being much more accessible than Electron's 150-200MB baseline.
| Aspect | Deno Desktop | Electron | Tauri |
|---|---|---|---|
| Language | TypeScript / JavaScript | JavaScript + Node.js | Rust backend + JS frontend |
| Min App Size | ~19 MB (compressed, WebView) | ~150 MB | ~3 MB |
| Toolchain Setup | Just Deno | Node.js + npm | Rust + Cargo + system deps |
| Rendering | WebView or CEF (configurable) | Bundled Chromium (always) | OS WebView (WKWebView/WebView2) |
| Native APIs | Built-in Deno.* namespace | Node.js + electron modules | Rust commands via IPC |
| Cross-Compilation | Built-in | Per-platform build agents | Per-platform build agents |
| Framework Auto-Detect | Built-in (10+ frameworks) | Manual setup | Manual setup |
| Maturity | Experimental (2.9) | Mature (10+ years) | Stable (2.x) |
Deno desktop won't replace Electron for complex, mature products overnight — Electron's 10-year ecosystem of plugins, debugging tools, and community knowledge is hard to beat. But for new projects, especially internal tools, prototypes, and apps that don't need every Electron feature, Deno desktop offers a dramatically simpler development experience with a fraction of the bundle size.
If you're curious how Electron is evolving in response, see my comparison of Electron 43 beta — performance improvements and memory optimizations. For a broader overview of Deno's capabilities beyond desktop, check my Deno 2.8 guide covering all subcommands and features.
Let me walk through a complete example — a simple desktop markdown notes app. This demonstrates the entrypoint, file system access, native dialogs, and the JS-to-Deno bridge.
Step 1: Entrypoint — main.ts
import { serve } from "jsr:@std/http/server";
let notes: Array<{ title: string; content: string }> = [];
const handler = (req: Request): Response => {
const url = new URL(req.url);
// API route: get notes
if (url.pathname === "/api/notes" && req.method === "GET") {
return Response.json(notes);
}
// API route: save note
if (url.pathname === "/api/notes" && req.method === "POST") {
const note = await req.json();
notes.push(note);
return Response.json({ ok: true });
}
// Serve the HTML UI
return new Response(html, {
headers: { "content-type": "text/html" },
});
};
serve(handler);
Step 2: HTML UI — embedded in the same file or served separately
const html = `<!DOCTYPE html>
<html>
<head>
<title>Notes App</title>
<style>
body { font-family: system-ui; max-width: 800px; margin: 0 auto; padding: 2rem; }
textarea { width: 100%; min-height: 200px; }
</style>
</head>
<body>
<h1>My Notes</h1>
<textarea id="editor" placeholder="Write your note..."></textarea>
<button onclick="saveNote()">Save</button>
<div id="notes"></div>
<script>
async function saveNote() {
const content = document.getElementById('editor').value;
await fetch('/api/notes', {
method: 'POST',
body: JSON.stringify({ title: 'Note', content })
});
loadNotes();
}
async function loadNotes() {
const res = await fetch('/api/notes');
const notes = await res.json();
document.getElementById('notes').innerHTML =
notes.map(n => '<div>' + n.content + '</div>').join('');
}
loadNotes();
</script>
</body>
</html>`;
export { html };
Step 3: Build and distribute
# Development
deno desktop main.ts --hmr
# Production build (compressed)
deno desktop main.ts --compress --target x86_64-apple-darwin
This is a working desktop app in about 60 lines of TypeScript and HTML. The same app
in Electron would require a main.js, a preload.js, a
package.json, and about 150+ lines of boilerplate before you even start
writing application logic.
Once you have a compiled binary, distribution options include:
Deno.autoUpdate() to ship incremental
binary patches, avoiding full re-downloads for minor updates.Deno desktop is experimental in 2.9. Before building a production application, consider:
Deno.BrowserWindow and the desktop APIs
are stabilizing but may change between 2.9 and the stable release.Deno.Dock) are
macOS-only. Windows and Linux equivalents are still landing.deno desktop is a new Deno 2.9 subcommand (experimental) that turns JavaScript and TypeScript projects into native, self-contained desktop applications for macOS, Windows, and Linux. It replaces the need for Electron or Tauri by integrating a WebView or Chromium backend directly into Deno.
--compress option can shrink the full bundle from ~65MB to ~19MB. There's no Node.js layer — your Deno scripts run directly with full access to Deno's native APIs.
deno desktop auto-detects Next.js, Astro, Fresh, Remix, Nuxt, SvelteKit, SolidStart, TanStack Start, and any Vite SSR project. Run deno desktop with no entrypoint and it builds and wraps your framework automatically. HMR is supported via the --hmr flag.
Deno.BrowserWindow for window control (size, position, menus, DevTools), Deno.Tray for system tray icons, Deno.Dock for macOS dock integration, and Deno.autoUpdate for background binary updates. prompt(), alert(), and confirm() render as native OS dialogs automatically.
deno desktop is experimental in Deno 2.9 (released June 25, 2026). The core API surface is stabilizing but some platform features are still landing. It's ready for prototyping and internal tools. For production distribution, test thoroughly on target platforms and watch for the stable release in 2.10+.
--compress flag compiles down to approximately 19MB. Without compression, the same app is around 65MB. A minimal Electron app is typically 150-200MB, and a Tauri app is around 3-5MB. For more on Deno's overall capabilities, see my Deno 2.8 guide.
I build full-stack web applications and can help evaluate whether Deno desktop, Electron, or Tauri is the right fit for your project. Get in touch for a free consultation.