Articles

How to Handle Browser Back Button Navigation Events in Vue 3

Dec 2 7 min read vue
vuecomposabletips&tricks

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?

  1. User opens a modal dialog
  2. User clicks the browser's back button
  3. Instead of closing the modal, the whole application navigates back

Or maybe:

  1. User opens a modal
  2. Modal closes when back button is pressed (good!)
  3. 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

useBackButton.vue
// 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

example.vue
<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

example.vue
<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

It is important to handle edge cases such as double click, removing listener events & global state to ensure a smooth user experience.

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

  1. Always Use Refs Pass reactive refs to the composable for reliable state management.
  2. Router Guard Integration Implement the router guard to ensure consistent navigation behavior.
  3. Clean Unmounting The composable handles its own cleanup, but ensure parent components handle their state properly.
  4. 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

All rights reserved.
Hasib • © 2026