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.
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
scopeopens inside the PWA, even withtarget="_blank". - Result: forward navigation has to be handled actively.
Comparison
| Behavior | Android (Chromium) | iOS (WebKit) |
|---|---|---|
| Back | System button or gesture | Left-edge swipe |
| Forward | Not available | Right-edge swipe |
| External links | Chrome Custom Tab | In-app Safari view |
In-scope target="_blank" | May open in a Custom Tab | Stays inside the PWA |
| Native feel by default | High | Needs 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.htmlopens in the same browsing context. The history stack now holdsapp.htmlandpage2.html, so the user can swipe back and swipe forward. - With
target="_blank":page3.htmlopens in a new browsing context. Because the URL is inside the manifestscope, 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