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.
Have you ever encountered these scenarios?
Or maybe:
These are common issues that can frustrate users and make your application feel unprofessional. Let's solve them with a robust solution.
Our useBackButton composable is designed to:
Here's how we build it:
// 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,
}),
});
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.
We implement smart scroll behavior:
if ("scrollRestoration" in history) {
history.scrollRestoration = "manual";
}
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.
The router guard prevents unwanted navigation:
router.beforeEach((to, from, next) => {
if (globals.isBackBtnPressed) {
globals.isBackBtnPressed = false;
next(false);
return;
}
next();
});
<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>
<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>
We handle rapid back button clicks by adding two history states:
history.pushState(null, "", window.location.pathname);
history.pushState(null, "", window.location.pathname);
Proper cleanup is essential to prevent memory leaks:
onUnmounted(() => {
if ("scrollRestoration" in history) {
history.scrollRestoration = "auto";
}
window.removeEventListener("popstate", handlePopState);
});
We use small timeouts to ensure proper state management:
setTimeout(() => {
globalStore.isBackBtnPressed = false;
}, 100);
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:
With these considerations in mind, you'll have a reliable solution for handling browser navigation in your Vue 3 applications.
PrimeVue Style Override
The PrimeVue UI component library provides a wide range of components with their own design. In some cases, we need to customize them to better suit our own needs. There are different ways we can override the default styles of PrimeVue, but we need to keep a few things in mind to reduce headaches later down the line 😅.
Unique approach of CSS custom property great flexibility and modularity
CSS Custom Properties offers great flexibility and modularity to a stylesheet. Allows developers to centralize values, simplify maintaining and updating multiple elements at once across the entire project. The use of custom properties can go from simple color, shadow values to do dynamic changes to styles based on user interactions or other events using JavaScript.