Ship a desktop app
Draw your own window chrome in a Tauri v2 or Electron app with app-window, wired to the real window actions.
On this page
app-window is the window chrome of a real app: a title bar that drags the window, maximizes on double-click, and carries the controls the platform draws. It has no Tauri or Electron dependency. You pass the actions in.
pnpm dlx shadcn@latest add @ballmac/app-window @ballmac/feel-autonpx shadcn@latest add @ballmac/app-window @ballmac/feel-autoyarn dlx shadcn@latest add @ballmac/app-window @ballmac/feel-autobun x shadcn@latest add @ballmac/app-window @ballmac/feel-autoUse it in Tauri v2
Turn off the native frame
{
"app": {
"windows": [{ "title": "Notes", "decorations": false }]
}
}Allow the window actions
{
"identifier": "default",
"windows": ["main"],
"permissions": [
"core:default",
"core:window:allow-close",
"core:window:allow-minimize",
"core:window:allow-toggle-maximize",
"core:window:allow-is-maximized",
"core:window:allow-set-fullscreen",
"core:window:allow-start-dragging"
]
}Wire the buttons
import { getCurrentWindow } from "@tauri-apps/api/window"
import { AppWindow, AppWindowContent, AppWindowTitleBar } from "@/components/ballmac/app-window"
const win = getCurrentWindow()
export default function App() {
const [maximized, setMaximized] = useState(false)
useEffect(() => {
win.isMaximized().then(setMaximized)
const unlisten = win.onResized(() => win.isMaximized().then(setMaximized))
return () => void unlisten.then((f) => f())
}, [])
return (
<AppWindow
className="h-screen rounded-none border-0"
maximized={maximized}
onClose={() => win.close()}
onMinimize={() => win.minimize()}
onMaximize={() => win.toggleMaximize()}
onFullscreen={async () => win.setFullscreen(!(await win.isFullscreen()))}
>
<AppWindowTitleBar title="Notes" />
<AppWindowContent>…</AppWindowContent>
</AppWindow>
)
}Use it in Electron
Create the window without a frame and handle the actions in the main process:
const win = new BrowserWindow({ frame: false, webPreferences: { preload: join(__dirname, "preload.js") } })
ipcMain.on("window", (event, action: "close" | "minimize" | "maximize") => {
const w = BrowserWindow.fromWebContents(event.sender)
if (!w) return
if (action === "maximize") w.isMaximized() ? w.unmaximize() : w.maximize()
else w[action]()
})Expose a small bridge in the preload script:
contextBridge.exposeInMainWorld("windowControls", {
send: (action: "close" | "minimize" | "maximize") => ipcRenderer.send("window", action),
})Pass the actions to the window in the renderer:
<AppWindow
onClose={() => window.windowControls.send("close")}
onMinimize={() => window.windowControls.send("minimize")}
onMaximize={() => window.windowControls.send("maximize")}
/>The title bar sets both data-tauri-drag-region and -webkit-app-region: drag, and the toolbar inside it is marked no-drag, so buttons stay clickable.
Start from the desktop blocks
The free desktop blocks (preferences, menu bar and tray, software update, licence activation, permissions setup, About, welcome window and shortcuts editor) draw real window chrome with app-window and handle like macOS or Windows from the page's feel. The AI notes on the software update block show how to wire tauri-plugin-updater and electron-updater.