handle_links: opting an installed PWA into link handling
In one line: handle_links is a proposed web app manifest member that lets an installed
PWA state a preference for whether in-scope links should open inside the installed app
rather than the browser, via three values — auto, preferred, and not-preferred.
What the three values mean
Section titled “What the three values mean”Per the WICG explainer, the member’s shape is "handle_links": "auto" | "preferred" | "not-preferred":
preferred— the user agent should open in-scope links within the installed application.not-preferred— the user agent should not open links within the installed application.auto— the user agent should select the appropriate behavior for the platform. This is the default value ifhandle_linksis not found in the manifest.
These are stated as suggestions from the app to the user agent, not a hard guarantee.
Where it fits among the manifest’s link members
Section titled “Where it fits among the manifest’s link members”The explainer’s Non-Goals section is explicit that handle_links does not do everything a
link-handling PWA might need:
- It does not extend which origins count as in-scope — that is
scope_extensions, whichhandle_linkscan be combined with once a link’s origin has been brought into scope. - It does not customize what happens once the app launches (routing, window placement) —
that is
launch_handler. handle_linksonly governs whether an in-scope link opens the installed app at all.
Browser & ecosystem support
Section titled “Browser & ecosystem support”Chrome’s own tracking entry on chromestatus.com lists its implementation status as
“On hold”, with no origin trial. The same tracking data shows “No signal”
positions from both Firefox and Safari. MDN’s web app manifest reference page does not list
handle_links among the manifest members it documents. Treat it as a proposal-stage member
whose only tracked status across the three engines is “on hold” or “no signal” — check
chromestatus.com and MDN again before depending on it.
Example
Section titled “Example”{ "name": "Example PWA", "start_url": "/app/", "scope": "/app/", "handle_links": "preferred"}Per the Web App Manifest spec’s scope-processing rules,
if start_url is not within the declared scope, the user agent ignores scope and falls
back to a default scope derived from start_url instead — so start_url must stay inside
scope for the declared boundary to actually apply.
Detecting effective link handling
Section titled “Detecting effective link handling”The WICG explainer describes handle_links only as a manifest member read by the browser;
it defines no companion JavaScript property or
runtime API for reading it back, and no feature-detectable object or CSS media feature exists
for the member itself. Checking display-mode does not detect handle_links support — it
only tells a page which display mode it is currently rendered in, which is a weak, unrelated
signal of “looks like an installed app.” Per MDN, a PWA sets its display mode by setting the
manifest’s display member, and the reported display-mode value reflects standalone or
minimal-ui when the browser honors that request (MDN notes this may not match what was
requested, since a browser can decline to honor it). MDN also documents
window-controls-overlay as a further standalone-style desktop mode. fullscreen and
picture-in-picture, by contrast, can also be triggered by any web app at runtime via the
Fullscreen API or the Document Picture-in-Picture API — so a match there is ambiguous with an
installed app and is not a useful signal on its own.
Because the explainer defines no runtime detection mechanism for handle_links, keep regular
in-page navigation working regardless of whether the browser honors
handle_links, and use display-mode only to adjust presentation (not navigation logic):
const installedModes = ["standalone", "minimal-ui", "window-controls-overlay"];const looksInstalled = window.matchMedia && installedModes.some((mode) => window.matchMedia(`(display-mode: ${mode})`).matches);if (looksInstalled) { // Rendered in standalone, minimal-ui, or window-controls-overlay — a // signal, not a guarantee, of running as the installed app. Adjust // presentation only, e.g. hide a browser-only "Install" button: document.querySelectorAll("[data-install-prompt]").forEach((el) => el.hidden = true);} else { // Rendered in browser, fullscreen, or picture-in-picture — the default // mode, or a mode any web app (installed or not) can also enter at // runtime. This signal cannot confirm whether handle_links is // unsupported or simply inactive, so show the browser-only "Install" // button instead of hiding it: document.querySelectorAll("[data-install-prompt]").forEach((el) => el.hidden = false);}// Either branch: in-page links keep their default anchor navigation — the// app never intercepts clicks based on handle_links, since its effective// state cannot be read back at runtime.Practical checklist
Section titled “Practical checklist”- Don’t rely on
handle_linksalone to route users into your installed app — its chromestatus.com tracking status is “On hold” in Chrome and “No signal” in Firefox and Safari. - If you add it now, treat it as a forward-looking declaration for a proposal-stage member — its only tracked engine status is “On hold” (Chrome, no origin trial) or “No signal” (Firefox, Safari).
- Combine it with
scope_extensionswhen the goal is opening links from other origins in the installed app —handle_linksalone only covers your existing scope. - Re-check chromestatus.com and MDN before depending on this in production; the explainer is still WICG-stage and unshipped.
Where to go next
Section titled “Where to go next”- Manifest scope — the scope boundary
handle_linksapplies within. - Manifest launch_handler — the related member that customizes what happens once the app launches.