Skip to main content

Disabling Forward Navigation in PWAs: iOS (WebKit) vs Android (Chromium)

The problem

A PWA on iPhone (WebKit) behaves very differently from one on Android (Chromium), especially around navigation and gestures. The goal is a native-app feel, and forward navigation is where iOS breaks it.

PWAs: iOS (WebKit) vs Android (Chromium)

Observations

Android (Chromium, WebAPK / Trusted Web Activity)

  • Back navigation only. The system back button or gesture goes back; there is no forward action. This is very close to native behavior, so there is nothing to fix.
  • External links open in a Chrome Custom Tab. Links to other origins, and target="_blank" links outside the scope, open in a Custom Tab. Links inside the scope usually stay in the app.
  • Result: no extra work is needed, because forward navigation is not exposed by default.

iOS (WebKit, standalone mode)

  • Edge swipe gestures. WebKit enables a swipe from the left edge (back) and the right edge (forward) in standalone PWAs. These trigger page-transition animations and history navigation that break the native-app feel.
  • External links open in an in-app Safari view (a modal sheet), while every URL inside the manifest scope opens inside the PWA, even with target="_blank".
  • Result: forward navigation has to be handled actively.

Comparison

BehaviorAndroid (Chromium)iOS (WebKit)
BackSystem button or gestureLeft-edge swipe
ForwardNot availableRight-edge swipe
External linksChrome Custom TabIn-app Safari view
In-scope target="_blank"May open in a Custom TabStays inside the PWA
Native feel by defaultHighNeeds work

The solution: Disabling Forward Navigation

There is no official API to turn off the edge-swipe gesture in iOS standalone PWAs. However, there is an undocumented behavior that works as a practical solution. It was found by testing on a real device and is not documented by Apple.

How it works

Imagine an app with app.html, which contains two links: page2.html (no target) and page3.html (with target="_blank").

  • Without target: page2.html opens in the same browsing context. The history stack now holds app.html and page2.html, so the user can swipe back and swipe forward.
  • With target="_blank": page3.html opens in a new browsing context. Because the URL is inside the manifest scope, iOS keeps it inside the PWA instead of opening the Safari modal view.

The result is two separate contexts. The old one holds only app.html, and the new one holds only page3.html. When the user swipes back from page3.html, they return to app.html, whose context has no other entries, so there is no forward history at all.

The rule is simple: always open in-scope pages with target="_blank", so that no context ever has a forward entry.

The Android caveat

On Android, target="_blank" opens the page in a Chrome Custom Tab, even inside the scope, which is not what you want. The fix is easy with JavaScript: apply the technique only on iOS standalone mode.

const isIOS =
  /iphone|ipad|ipod/i.test(navigator.userAgent) ||
  (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);

const isStandalone =
  navigator.standalone === true ||
  matchMedia("(display-mode: standalone)").matches;

if (isIOS && isStandalone) {
  document.addEventListener("click", (e) => {
    const a = e.target.closest("a[href]");
    if (!a || a.target === "_blank") return;

    const url = new URL(a.href, location.href);
    if (url.origin !== location.origin) return; // external links: leave untouched

    a.target = "_blank"; // in-scope link: force a new context on iOS
  });
}

Undocumented behavior:

Apple could change how in-scope _blank links are handled in any iOS update, and your fix would silently stop working. You’d have to retest after each major release.

0 comments:

Post a Comment

Blog Archive