Skip to content

Installation · Platform

Desktop installation

Published

Desktop installation turns a site into an OS-level app with its own window, launcher entry, and taskbar or Dock presence. Chrome 73 and Edge 79 install from the address bar, the browser menu, or beforeinstallprompt on Windows, macOS, Linux, and ChromeOS; Safari 17 on macOS 14 installs through File › “Add to Dock”; Firefox 143 on Windows pins a site as a taskbar web app (Firefox 143 release notes); earlier desktop Firefox and Safari have no install path at all, and no browser other than Chromium fires beforeinstallprompt (BCD api.BeforeInstallPromptEvent).

A desktop install has three parts: a trigger, an OS registration, and a window mode. The trigger is either the browser’s own surface (Chrome and Edge show an install icon at the right end of the address bar once the installability criteria are met, plus an install entry in the three-dot menu; Safari uses File › “Add to Dock”; Firefox 143 uses an address-bar icon) or, on Chromium only, the page’s own button backed by a stored BeforeInstallPromptEvent. There is no mini-infobar on desktop; without the event, a page has no ambient banner to suppress.

On accept, the browser writes an OS launcher entry so the app can start without the browser being open first:

OS What the browser creates Management surface
Windows (Chrome, Edge) Start-menu shortcut; the app appears in the taskbar and Alt+Tab. Edge also registers it under Settings › Apps › “Installed apps”. chrome://apps, edge://apps
macOS (Chrome, Edge) An app bundle in ~/Applications/Chrome Apps.localized/ or ~/Applications/Edge Apps.localized/, visible in Spotlight and the Dock. chrome://apps, edge://apps
macOS (Safari 17+) An app in ~/Applications/ opened by a Safari-based window without tabs or an address bar; cookies and storage are separate from Safari’s. Delete the app from Applications
Linux (Chrome, Edge) A .desktop entry in ~/.local/share/applications/, picked up by GNOME and KDE launchers. chrome://apps, edge://apps
ChromeOS A launcher entry that can be pinned to the shelf; Play Store listings via TWA are separate. Launcher › right-click › Uninstall

The window mode comes from the manifest. display: standalone yields a window with the OS title bar and no browser chrome; minimal-ui adds back, forward, and reload; display_override: ["window-controls-overlay"] lets the page draw into the title-bar area while the OS keeps the close, minimise, and maximise buttons, exposing the remaining area through env(titlebar-area-x), env(titlebar-area-y), env(titlebar-area-width), and env(titlebar-area-height) (Chrome 105, Edge 105, desktop only). Window title and icon come from name and icons; a new window per launch versus focusing the existing one is decided by launch_handler.client_mode.

The trade-off against a browser tab is concrete: the app loses the address bar, tab strip, and extensions (Chrome runs installed apps without extension toolbars), and gains a separate window in Alt+Tab or Cmd+Tab, OS notifications attributed to the app’s name and icon, badging, file and protocol handling, and launch from the OS. Cookies and storage are shared with the installing browser profile on Chrome and Edge, so sign-in carries over; on Safari 17 they are not, so the user signs in again inside the app.

The installed artefact is visible on disk, which is the quickest way to confirm what a given browser did.

A page can tell that it is running installed, and which window mode it got, without any browser-specific API. The media query reports the mode the browser applied, which may differ from the one requested when display_override lists several.

const mode = ['window-controls-overlay', 'standalone', 'minimal-ui', 'fullscreen']
.find((m) => matchMedia(`(display-mode: ${m})`).matches) ?? 'browser';
if (mode !== 'browser') {
installButton.hidden = true; // already installed and launched as an app
}
if (mode === 'window-controls-overlay') {
document.documentElement.classList.add('wco'); // draw into the title-bar area
}

Safari 17 and Firefox 143 apps also report standalone here, so the check needs no user-agent branch; what differs per browser is only how the user got there.

Specifications

SpecificationStatus
None.