How to Handle Browser Back Button Navigation Events in Vue 3
As web applications become more sophisticated, managing browser navigation becomes increasingly complex. One common challenge is handling the browser's back button when dealing with modals, dialogs, or drawers. Today, we'll explore a powerful Vue 3 composable that solves this problem while maintaining a smooth user experience.
The Challenge
Have you ever encountered these scenarios?
- User opens a modal dialog
- User clicks the browser's back button
- Instead of closing the modal, the whole application navigates back
Or maybe:
- User opens a modal
- Modal closes when back button is pressed (good!)
- But the page unexpectedly scrolls to the top (bad!)
These are common issues that can frustrate users and make your application feel unprofessional. Let's solve them with a robust solution.
Introducing useBackButton Composable
Our useBackButton composable is designed to:
- Intercept back button clicks
- Close modals/dialogs instead of navigating
- Preserve scroll position when appropriate
- Handle edge cases like double-clicks
- Maintain a clean browser history
Here's how we build it:
The Core Implementation
// composables/useBackButton.js
import { watch, onMounted, onUnmounted } from "vue";
import { useGlobalStore } from "@/stores/global";
export function useBackButton(contentVisibleRef) {
const globalStore = useGlobalStore();
const handlePopState = () => {
if (contentVisibleRef.value) {
// Add history.pushState first to prevent navigation
history.pushState(null, "", window.location.pathname);
history.pushState(null, "", window.location.pathname); // Add twice to ensure it works with double back click
globalStore.isBackBtnPressed = true;
contentVisibleRef.value = false;
}
};
onMounted(() => {
// Add initial history state
history.pushState(null, "", window.location.pathname);
window.addEventListener("popstate", handlePopState);
});
onUnmounted(() => {
window.removeEventListener("popstate", handlePopState);
});
watch(contentVisibleRef, (newValue) => {
if (!newValue) {
setTimeout(() => {
globalStore.isBackBtnPressed = false;
}, 100);
}
});
watch(
() => globalStore.isBackBtnPressed,
(newValue) => {
if (newValue) {
// Ensure route change is prevented when state is true
history.pushState(null, "", window.location.pathname);
}
},
);
}
// router guard
router.beforeEach((to, from, next) => {
if (globals.isBackBtnPressed) {
globals.isBackBtnPressed = false; // Reset immediately
next(false); // Prevent navigation
return;
}
next();
});
// global store
export const useGlobalStore = defineStore("global", {
state: () => ({
isBackBtnPressed: false,
}),
});
How It Works
1. History State Management
The composable uses the History API to manage browser navigation. When mounted, it adds an initial state:
onMounted(() => {
history.pushState(null, "", window.location.pathname);
window.addEventListener("popstate", handlePopState);
});
This creates a "buffer" state that we can use to detect back button clicks.
2. Scroll Position Management
We implement smart scroll behavior:
- Preserve scroll position when closing modals via back button
- Allow normal scroll-to-top behavior for regular navigation
if ("scrollRestoration" in history) {
history.scrollRestoration = "manual";
}
3. State Management with Pinia
We use a global store to track back button states:
export const useGlobalStore = defineStore("global", {
state: () => ({
isBackBtnPressed: false,
}),
});
This helps coordinate between the composable and router guards.
4. Router Integration
The router guard prevents unwanted navigation:
router.beforeEach((to, from, next) => {
if (globals.isBackBtnPressed) {
globals.isBackBtnPressed = false;
next(false);
return;
}
next();
});
Usage Examples
Basic Modal Implementation
<template>
<div>
<button @click="showModal = true">Open Modal</button>
<Modal v-if="showModal" v-model="showModal"> Content here </Modal>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useBackButton } from "@/composables/useBackButton";
const showModal = ref(false);
useBackButton(showModal);
</script>
With Complex Dialog
<template>
<div>
<button @click="openDialog">Open Complex Dialog</button>
<Dialog v-model="dialogVisible" :options="dialogOptions">
<template #default>
<form @submit.prevent="handleSubmit">
<!-- Form content -->
</form>
</template>
</Dialog>
</div>
</template>
<script setup>
import { ref } from "vue";
import { useBackButton } from "@/composables/useBackButton";
const dialogVisible = ref(false);
const dialogOptions = ref({
title: "Complex Form",
width: "600px",
});
useBackButton(dialogVisible);
</script>
Edge Cases and Solutions
1. Double Back Click Handling
We handle rapid back button clicks by adding two history states:
history.pushState(null, "", window.location.pathname);
history.pushState(null, "", window.location.pathname);
2. Cleanup
Proper cleanup is essential to prevent memory leaks:
onUnmounted(() => {
if ("scrollRestoration" in history) {
history.scrollRestoration = "auto";
}
window.removeEventListener("popstate", handlePopState);
});
3. Timing Issues
We use small timeouts to ensure proper state management:
setTimeout(() => {
globalStore.isBackBtnPressed = false;
}, 100);
Best Practices
- Always Use Refs Pass reactive refs to the composable for reliable state management.
- Router Guard Integration Implement the router guard to ensure consistent navigation behavior.
- Clean Unmounting The composable handles its own cleanup, but ensure parent components handle their state properly.
- Testing
Test various scenarios:
- Rapid back button clicks
- Modal opening/closing sequences
- Navigation during modal open states
Conclusion
The useBackButton composable provides a robust solution for handling browser navigation in modern Vue applications. It solves common UX issues while maintaining clean code architecture and providing a seamless user experience.
Whether you're building a simple modal dialog or a complex multi-step form, this composable can help you manage browser navigation effectively while preserving user context and scroll position.
Remember to:
- Test thoroughly in your specific use case
- Consider edge cases specific to your application
- Maintain proper state management
- Handle cleanup appropriately
With these considerations in mind, you'll have a reliable solution for handling browser navigation in your Vue 3 applications.
Thank You
Continue Reading
Adding Custom Button Variants in Nuxt UI
Learn how to extend Nuxt UI's button component with custom variants like a stunning skew animation effect. We'll explore app.config.ts configuration, CSS pseudo-elements, and compound variants to create unique button styles that match your brand.
From Commit to Release: Automating GitHub Workflows
This documentation describes an automated GitHub workflow system that handles pull request labelling and automated releases for both development and production environments. The system uses branch naming conventions to automatically label PRs, generates release notes, and manages versioning.