Tested with: Ionic 8, Angular 21, Capacitor 7, iOS Safari PWA (standalone).
Ionic apps work well in Capacitor, but the same app installed as a PWA on iOS can have problems with swipe-to-go-back (STGB).
On iOS PWA, Safari's native STGB and Ionic's page transitions fight each other. This post is about fixing that conflict so you get no double animations, no weird flashes, and behavior that holds up in daily use.
Capacitor: no problem
In a native Capacitor app, STGB works as expected. Capacitor turns off Safari's native edge swipe so Ionic's own STGB can handle the gesture instead, and you do not get double animations or page flashes.
Add this to app.config.ts:
provideIonicAngular({
swipeBackEnabled: Capacitor.isNativePlatform(),
})
In Capacitor, Ionic STGB is enabled and native Safari STGB is disabled, which is enough.
iOS PWA: two systems conflict
In Safari and installed PWAs, Apple keeps native STGB enabled and your app cannot turn it off. Ionic also runs its own STGB and page transitions, so both systems react to the same swipe.
What you see:
- Double animations. Safari slides the page, then Ionic slides it again.
- Weird flashes. After you swipe back, the page you left can flash on screen again.
Setting swipeBackEnabled: false on iOS web is not enough. Ionic can still animate a back navigation that was started by the browser's native history, so the conflict remains.
Ionic is built mainly for Capacitor here, not for iOS PWA. Apple could add an API to turn off native STGB, but we could not wait for that.
What we wanted
We wanted to keep page animations where they help and stop Ionic from animating back when Safari already handled the gesture.
On iOS PWA, we use these rules:
- Native STGB → Ionic must do nothing. No second animation.
- Toolbar back button → Ionic may animate. The user expects it.
- Programmatic
navigateBack→ same as toolbar.
The fix: gate Ionic back animations
We added NavigationAnimationControl, a small service registered with ENVIRONMENT_INITIALIZER in app.config.ts. It only runs when platform.is('ios') && !Capacitor.isNativePlatform().
export const navigationAnimationControlProvider = {
provide: ENVIRONMENT_INITIALIZER,
multi: true,
useValue: () => inject(NavigationAnimationControl),
};
It patches the injected NavController instance at runtime, replacing consumeTransition and navigateBack with wrappers around the originals.
private patchIonicTransitions(): void {
this.navController.consumeTransition = () =>
this.controlTransition(this.originalConsumeTransition());
this.navController.navigateBack = (...parameters) =>
this.navigateBack(...parameters);
}
private controlTransition(transition: IonicTransition): IonicTransition {
if (transition.direction !== 'back') {
return transition;
}
if (this.consumeBackAnimationAllowed()) {
return transition;
}
return { ...transition, animation: undefined };
}
private navigateBack(...parameters: Parameters<NavController['navigateBack']>) {
this.allowNextBackAnimation();
return this.originalNavigateBack(...parameters);
}
consumeTransition— for back transitions, removeanimationunless a back animation was clearly allowed. This stops the double animation when Safari already moved the page.navigateBack— allow the next back animation, then call the original method.
Back animation is allowed when:
- The user clicks
ion-back-button. Our shared page wrapper callsallowNextBackAnimation()on click:
<ion-back-button
[defaultHref]="backHref"
(click)="navigationAnimation.allowNextBackAnimation()"
></ion-back-button>
- Code calls
navController.navigateBack(...). The patched method sets the flag automatically.
For everything else — native STGB, browser back, and popstate — Ionic gets animation: undefined because Safari already moved the page and Ionic only needs to update its stack without another animation.
Unused allowances are cleared on NavigationEnd, NavigationCancel, or NavigationError, which prevents a cancelled click from allowing the next navigation by mistake.
Forward-then-back race
Even with the patch, a quick swipe back right after a forward navigation could still flash. We track when a forward animation starts and block back animation for 600ms to give the stack time to settle:
private static readonly forwardAnimationSettleMs = 600;
private controlTransition(transition: IonicTransition): IonicTransition {
if (transition.direction !== 'back') {
this.trackForwardAnimation(transition);
return transition;
}
if (this.consumeBackAnimationAllowed() && !this.forwardAnimationSettling()) {
return transition;
}
return { ...transition, animation: undefined };
}
This helps but does not fully fix it. If you navigate forward and swipe back while the forward animation just started, the forward page can still flash for a few milliseconds. We may tackle that in a later post.
Conclusion
It works, but it is not simple. The consumeTransition patch is about 100 lines and has been stable in daily use: toolbar back animates, and native swipe back no longer gets a second Ionic animation.
It is still not perfect. The fix depends on Ionic internals, and Ionic does not prioritize iOS PWA for this behavior. These runtime patches to NavController could break after an Ionic upgrade, and the 600ms forward-settle window is a practical guess rather than a proven value. There may still be race conditions or rare flashes on paths we have not tested.
If you ship an Ionic PWA on iOS and care about polish, you will likely need to handle this yourself until Apple, Ionic, or both provide a cleaner way to avoid this conflict.