Ballmac UI home

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-auto

Use it in Tauri v2

Turn off the native frame

src-tauri/tauri.conf.json
{
  "app": {
    "windows": [{ "title": "Notes", "decorations": false }]
  }
}

Allow the window actions

src-tauri/capabilities/default.json
{
  "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

src/App.tsx
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:

main.ts
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:

preload.ts
contextBridge.exposeInMainWorld("windowControls", {
  send: (action: "close" | "minimize" | "maximize") => ipcRenderer.send("window", action),
})

Pass the actions to the window in the renderer:

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.