Skip to main content

Ship a desktop app

Ship the shell as a native desktop app. The CLI uses Tauri 2 and writes the wrapper into dist/app/ so you do not commit native project files.

Browser PWA vs native (iOS)​

The same React / Vite shell runs in both hosts. Status-bar behavior does not:

HostHow it runsTop chrome
Safari / Home Screen PWAStandalone web appiOS 26/27 Liquid Glass — system status sampling / gradient. Shellui skips stacking its own status scrims and does not set theme-color.
Tauri iOS (App Store)Native WKWebViewNo Home Screen PWA system gradient. You control insets with viewport-fit=cover + env(safe-area-inset-*) (already in the shell).
Tauri desktopNative webviewOverlay titlebar / traffic lights (macOS).

Detect the live native shell in app code with window.__TAURI__ (or Shellui’s isTauriRuntime()). Prefer that over the CLI --target tauri build flag when deciding UI — a browser tab of a tauri-targeted build must not get native chrome.

import { isTauriRuntime, isHomeScreenPwa } from '@shellui/core';

if (isTauriRuntime()) {
// App Store / desktop WKWebView — native-controlled chrome
}

if (isHomeScreenPwa()) {
// Safari "Add to Home Screen" only
}

html[data-shellui-host] is set to browser | pwa | tauri for CSS.

App Store recommendation: distribute the iOS build via Tauri so you avoid the iOS Home Screen PWA status gradient. Keep the web PWA for install-from-Safari users who accept platform chrome. Always verify on a real iPhone/iPad — Tauri mobile has had its own inset quirks.

The shell document already ships:

<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>

Layouts pad with --shellui-safe-area-* (env(safe-area-inset-*)). On iOS WKWebView, if UIKit still shrinks the scroll view, set contentInsetAdjustmentBehavior = .never on the webview (e.g. via a Tauri iOS insets plugin) so edge-to-edge paint matches CSS safe-area.

Prerequisites​

  1. Rust via rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Confirm with cargo --version and rustc --version. If those fail, restart the terminal or run source ~/.cargo/env. failed to run 'cargo metadata' means Cargo is missing or off PATH.

  1. Node.js 18+ and a package manager.

Platform extras - see Tauri prerequisites:

  • macOS: Xcode Command Line Tools (xcode-select --install)
  • Windows: Visual Studio C++ Build Tools, WebView2
  • Linux: webkit2gtk and related packages listed in Tauri docs

Commands​

shellui dev --app
shellui build --app
npx shellui build --app --bundles app,dmg

shellui start --app is the same as dev --app. On first run the CLI generates dist/app/, installs desktop build tools if needed (for example @tauri-apps/cli), and syncs root tauri.conf.json plus shellui.config.json port into the wrapper.

CommandOutput
shellui builddist/web/ static site
shellui build --appdist/web/ plus .app on macOS under dist/app/
shellui build --app --bundles app,dmgAbove plus a .dmg

Everything under dist/ is generated and gitignored.

Configuration​

Optional tauri.conf.json next to shellui.config.json:

{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Shellui",
"identifier": "com.shellui.app",
"bundle": {
"icon": ["static/icon.png"]
}
}
FieldUse
productNameDock / installer / window name for bundled apps. Defaults to package.json name with the first letter capitalized. During tauri dev, macOS uses the Cargo package name (kept in sync from productName)
identifierBundle id. Defaults from the product name
bundle.iconSource icon(s) relative to the project root (prefer an opaque static/icon.png). The CLI runs tauri icon
app.windows[0]Optional window overrides. Overlay titlebar defaults stay unless you override them

From shellui.config.json: port is the dev URL (http://localhost:<port>); title is a fallback product name; favicon / appIcon are icon fallbacks when bundle.icon / static/icon.png are absent (appIcon is last and is a transparent chrome glyph). Prefer root tauri.conf.json for dock name and icon; keep title / appIcon for in-app chrome.

On macOS, the window uses an overlay titlebar. Close / minimize / zoom are system traffic lights. Shellui centers them in the 42px sidebar chrome; web controls get a 2px top pad. Traffic-light horizontal inset applies only in a live Tauri webview on macOS while the window is not native fullscreen (Tauri plugin:window|is_fullscreen + tauri://resize). Opening the same --target tauri URL in a browser, or entering fullscreen, drops that spacing. Back / Forward stay in Tauri chrome (including native fullscreen). A full-width invisible 42px top strip (data-tauri-drag-region; needs core:window:allow-start-dragging) is mounted at the app root. Buttons sit above that strip so they stay clickable. Sidebar Back walks iframe history, including out of a login page.

Bundle targets​

shellui build --app passes --bundles to Tauri. Default is app only (on macOS, .app under dist/app/src-tauri/target/release/bundle/macos/). That skips flaky DMG packaging during local builds.

npx shellui build --app --bundles app,dmg

DMG lands under dist/app/src-tauri/target/release/bundle/dmg/. Other Tauri bundle targets (deb, rpm, appimage, msi, nsis) work the same way on Linux or Windows.

{
"scripts": {
"build:app": "shellui build --app",
"build:app:dmg": "shellui build --app --bundles app,dmg"
}
}

Icons​

Sync prefers tauri.conf.json → bundle.icon, then static/icon.png, then favicon / static/favicon.svg (not the transparent appIcon chrome glyph). The CLI generates PNG / ICNS / ICO via tauri icon, then falls back to bundled defaults. Use a solid-background square for the dock, padded to Apple's ~824/1024 grid on macOS. Keep appIcon as the mono mark for sidebar / app-bar chrome.

Layout​

my-project/
├── shellui.config.json
├── static/
└── dist/
├── web/
└── app/

When developing Shellui itself, the monorepo exposes pnpm tauri:dev and pnpm tauri:build via tools/tauri/. Published-CLI apps use shellui dev --app and shellui build --app.

Troubleshooting​

failed to open icon .../icon.png. Remove dist/app and run shellui dev --app again.

bundle_dmg.sh failed (macOS). Look for .app under dist/app/src-tauri/target/release/bundle/macos/ - the app bundle can succeed when DMG packaging fails. Default --app skips DMG. If you requested app,dmg and it failed, unmount stale volumes (hdiutil info, then hdiutil detach /dev/diskX). This is a known flaky Tauri step.