After a lot of experimentation, I found that handling Bootstrap 5.3 modals in multi‑page apps requires some extra care. The key issues were:
BFCache and Navigation: When a user navigates back, the page can be restored from the BFCache. In these cases, events like popstate might not fire, but pageshow does (with event.persisted set to true). This allows you to detect that the page was restored.
Race Conditions: There was a race condition where the modal’s shown.bs.modal event fired after I requested a hide(). I solved this by using a flag (shouldBeHidden) and a one‑time listener on shown.bs.modal so that if the modal finishes showing while a hide was requested, it is immediately hidden.
Fade Animation: I temporarily remove the fade class in the force‑hide method to ensure that any ongoing animation doesn’t interfere with closing the modal. In this example, I want the modal to fade-in, but not fade out.
I hope this helps anyone facing similar issues with Bootstrap 5 modals in a multi‑page application! If you have a more elegant or reliable solution, please share so we can all learn from one another.
Tested this solution on: Windows: Chrome, Firefox, Android: Chrome, Firefox IOS: Safari
// PreloaderModal.js (JS Module)
export default class PreloaderModal {
/**
* Initialize the modal.
*/
constructor() {
// Define the modal element
this.modalEl = document.getElementById( "processingModal" );
// Create a new modal instance in Bootstrap
this.modalInstance = new bootstrap.Modal( this.modalEl, {
backdrop: 'static',
focus: true,
keyboard: false
} );
// This flag to determine if the modal should be hidden, defaults to no.
this.shouldBeHidden = false;
// Bind event handlers to ensure proper context
this.onPageShow = this.onPageShow.bind( this );
}
/**
* Show the modal.
*/
show() {
// Reset the flag
this.shouldBeHidden = false;
// add fade from the classlist, just in case it was removed
this.modalEl.classList.add( 'fade' );
// Show the modal.
this.modalInstance.show();
// Add event listeners.
window.addEventListener( 'pageshow', this.onPageShow );
// attach a one-time listener to the modal for the shown event.
// there's a possibility of a race condition where the modal is shown but should be hidden
// if that happens, we hide it immediately.
this.modalEl.addEventListener('shown.bs.modal', () => {
if ( this.shouldBeHidden ) {
this._forceHide();
}
}, { once: true });
}
/**
* Hide the modal.
*/
hide() {
// Set a flag so that if the shown event fires later, we hide immediately.
this.shouldBeHidden = true;
this._forceHide()
}
/**
* Force-hide the modal.
*/
_forceHide() {
// remove fade from the classlist
this.modalEl.classList.remove( 'fade' );
// Using internal state check if necessary; note _isShown is not public API.
if (this.modalInstance._isShown) {
this.modalInstance.hide();
}
// As a fallback, force-remove the "show" class and backdrop.
this.modalEl.classList.remove('show');
const backdrop = document.querySelector('.modal-backdrop');
if ( backdrop ) {
backdrop.remove();
}
}
/**
* When a pageshow event is detected, check to see if it was persisted (user clicked back/forward button)
* if so, hide the modal
* @param {*} event
*/
onPageShow( event ) {
if ( event.persisted ) {
this.hide();
}
}
}
// Using the JS Module
import PreloaderModal from "./components/PreloaderModal";
( function() {
const preloader = new PreloaderModal();
// ... do form validation etc.
preloader.show();
// ... submit the form
})();