Wrapping a website in a desktop app with JavaScript
A search for how to convert a website to a desktop app with JavaScript returns two kinds of pages. One is a fifteen line tutorial that creates a window and calls loadURL on it. The other is a product page. Both skip the part that decides whether the project is worth starting, which is what happens after the window opens and who keeps it working six months from now. The code is the small half of this problem.
Nothing is actually converted
The phrase is misleading in a way that matters. The site keeps running where it already runs, served over HTTPS by the same server. What gets built is a container process that hosts a browser engine and points it at a URL. The site's own JavaScript does not change, does not get compiled, and does not know it is running inside anything unusual. The JavaScript being written is the shell around it.
There are two shapes this can take, and mixing them up causes most of the confusion in the tutorials.
The first is a thin shell that loads a remote URL. The app bundle contains the engine and a few dozen lines of setup. Every deploy of the website is instantly live in the app, because the app is just a client. Nothing in the release pipeline changes.
The second bundles the built front end inside the app and serves it from the local filesystem. This one can start without a network connection, but it now has a version. Every front end change requires shipping a new build to every user, the API endpoint has to be configurable, and requests that used to be same origin are now cross origin, which means the server needs to allow them.
Most people searching this term want the first shape and start reading documentation for the second. Deciding which one applies takes thirty seconds and saves a week.
The routes that use JavaScript
| Engine that ships | Code required | Offline capable | Typical bundle | |
|---|---|---|---|---|
| Electron | Chromium and Node.js, bundled | Yes, a main process file | Yes, if assets are bundled | Hundreds of megabytes |
| Tauri | System WebKit, not bundled | Yes, plus a Rust toolchain to build | Yes | Single digit megabytes |
| NW.js | Chromium and Node.js, bundled | Minimal, a manifest | Yes | Hundreds of megabytes |
| Nativefier | Electron, generated | None, one command | No, remote URL only | Hundreds of megabytes |
| Safari, Add to Dock | The engine macOS already runs | None | No | Nothing extra |
| Chrome, Install page as app | The Chrome already installed | None | No | Nothing extra |
Electron is the default answer because it is the one with the largest amount of writing about it. It bundles its own copy of Chromium and Node.js, so the rendering is identical on every machine and the main process can touch the filesystem, the menu bar, and the tray. The cost is that every app carries its own browser.
Tauri takes the opposite trade. On macOS it renders through WKWebView, the engine already present in the operating system, so the bundle is small. The consequence is that rendering follows whatever version of WebKit the user's macOS ships, and building requires a Rust toolchain even though the front end stays JavaScript.
NW.js predates Electron and works from a manifest rather than an entry script, which makes it quick to point at an existing page. Nativefier goes further and generates an Electron app from a single command with a URL. That project has been archived by its maintainers, so anything it produces sits on whichever Electron version was current when it was last updated. Before adopting any generator, look at the date of its most recent release, because an unmaintained wrapper is an unpatched browser.
What the tutorials leave out
The window opens in an afternoon. Everything after that is the actual project.
An app that anyone other than the author will run has to be signed with an Apple Developer ID and notarized, or macOS will refuse to open it with a message about an unidentified developer. That requires membership in the Apple Developer Program, which costs 99 US dollars per year, and a notarization step in every release. Skipping it is workable for a single machine and not workable for handing the app to a colleague.
Updates are the second half. A browser tab updates when the server does. A bundled app does not, so shipping fixes means either a hosted update feed with signed releases or asking people to download a new copy each time. For a thin shell that only loads a remote URL this problem mostly disappears, which is another argument for the first shape.
The third item is the one that gets ignored. Bundling Chromium means owning Chromium's security patches. When a browser vulnerability is disclosed, browsers update themselves within days. A wrapper only gets the fix when someone rebuilds it against a newer runtime and ships that build. An app built once and left alone in the Applications folder is a browser frozen at the date it was created.
The defaults that need changing
A shell that loads a remote page is running code from the network with more privilege than a browser tab would give it. Electron's own guidance is to keep Node integration off in the renderer, leave context isolation on, and enable the sandbox, which is the configuration recent versions default to. Anything that reaches into the operating system should go through a narrow preload bridge that exposes named functions, never the whole module system.
Navigation deserves the same treatment. Without a handler, a link to any external site opens inside the app window, where there is no address bar to show what is being viewed. Restricting navigation to the intended origin, and pushing everything else out to the default browser, is a few lines that prevents the app from quietly becoming a browser without a location bar. This is worth doing even for a site that is entirely under the author's control, because outbound links are not.
Three ways these projects break later
Support requests about wrapped sites cluster into a small number of shapes, and knowing them in advance turns a rebuild into a two minute edit.
The first is a URL that moves. An internal tool migrates to a different subdomain, a service renames itself, or a marketing redirect starts intercepting the entry point, and the app opens a redirect page or a blank window every time. Routes with a settings panel are fixed by editing the address. Routes that generated the app from a command line have no settings panel, so the fix is rebuilding, which is only quick if the exact command used the first time was written down.
The second is a sign in flow that leaves the window. Single sign on bounces through an identity provider on another domain, and a window with no address bar and no back button has nowhere to go when that step fails. Anything with a corporate login should be built once, driven through a complete sign in, and then left until the session expires so the renewal path gets tested too. A flow that breaks on day fourteen is far more expensive to discover than one that breaks on day one.
The third is an operating system upgrade changing how a browser engine stores data or which entitlements it needs. Routes that ride on the system engine follow the upgrade automatically. Routes that bundle their own engine follow it when somebody rebuilds them, which for an abandoned generator means never.
None of these argue against the approach. They argue for keeping the build reproducible, which mostly means a plain text note recording which route produced which icon, since six months later the answer is not visible from the Dock.
When writing the shell is worth it
Building the container by hand pays off in a specific set of cases, and they have a common shape: the app needs to do something the browser deliberately will not.
Distribution to other people is the clearest one. A signed app with a chosen name, icon, and menu bar is a product. A bookmark is not.
Deep integration is the second. Global keyboard shortcuts that work while another app is focused, a menu bar item, registering a custom URL scheme so links elsewhere open the app, associating a file type, reading and writing local paths without a picker dialog, or staying resident with the window closed. None of these are available to a page in a tab.
Controlled behaviour is the third. Kiosk mode, a fixed window size, disabling navigation entirely, or injecting a script into the page on every load, for example to hide a navigation bar that is redundant inside a dedicated window.
If none of these appear in the requirements, the honest conclusion is that no code is needed.
When it is not worth it
For one person, one site, and one Mac, the built in routes cover more than most people expect. Safari can add a page to the Dock as a standalone app with its own icon, its own cookie store, and its own entry in the notification settings, which is what makes a second account of the same service possible. Chrome can install a page as an app that runs in a windowed frame. Neither requires a toolchain, a certificate, or a maintenance plan.
The ratio is what to weigh. The shell is a hundred lines. The signing certificate, the notarization step, the update feed, the runtime upgrades, and the rebuild every time Chromium ships a security release are the rest of it, and they recur. A dedicated builder exists to absorb exactly that recurring half, and the range of sites people wrap is visible in the Supported services list, which is a reasonable way to check whether an unusual internal tool is a normal case or an edge case.
A test to run before writing any code
Write down what the finished app must do, then check each item against the cheap routes.
A separate Dock icon. A separate entry in the application switcher. A window with no address bar. A separate entry in the notification settings. A separate cookie store so two accounts of the same service can be open at once.
Built in routes deliver all five for zero effort. If the list stops there, the project is finished before it starts. If it also includes a custom icon, links that must not leak back into a browser, a fixed window, or handing the app to ten other people, then a builder covers it without a repository, and the Features page is the fastest way to see where that line falls. Writing the shell yourself is the right answer only when the requirement is genuinely unusual, such as local file access or a background process, and at that point the tutorial was never the hard part anyway.
What to change first
Take the one site that stays open all day and give it a standalone window today by the cheapest route available, then live with it for a week before deciding anything else. The gap that shows up in that week, whether it is a second account, an icon, or links escaping into a browser, is the requirement that tells you whether a builder such as Kagemusha is enough or a hand written shell is genuinely needed.
Frequently asked questions
Does the website need to be modified to run inside a JavaScript wrapper?
No. A wrapper loads the same URL a browser would, and the page's own code runs unchanged. Modification only becomes necessary when the front end assets are bundled into the app instead of loaded from the server, because the API endpoint then has to be configurable and the requests become cross origin.
Why is an Electron app several hundred megabytes for one page?
Electron ships its own copy of Chromium and Node.js inside every application bundle, so the size is the runtime rather than the page. Tauri avoids this by rendering through the WebKit engine already present in macOS, which produces a much smaller bundle at the cost of following the system's browser version.
Can a wrapper built this way be sent to someone else?
Only after it is signed with an Apple Developer ID and notarized by Apple. Without that, macOS blocks it as coming from an unidentified developer. Membership in the Apple Developer Program costs 99 US dollars per year, and notarization has to be repeated for every release.
Is a wrapped app less secure than the same site in a browser?
It can be, because a browser updates itself and a bundled runtime does not. An app built once and never rebuilt keeps whatever browser engine version it was created with, including known vulnerabilities. Keeping Node integration off, context isolation on, and navigation restricted to the intended origin removes the other common risks.