# next-navigation-guard: an unsaved-changes prompt for Next.js

> next-navigation-guard gives Next.js the thing Nuxt has had for years, a navigation guard that can intercept a route change and ask the user before discarding unsaved work, with a window.confirm shortcut or your own dialog component.

**LayerXcom/next-navigation-guard** — Cancel page navigation in Next.js, like Nuxt's Navigation Guard feature.

- Repository: https://github.com/LayerXcom/next-navigation-guard
- Stars: 598 · Forks: 38
- Language: TypeScript
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/layerxcom-next-navigation-guard

## The problem it solves is one line long

The library's own pitch is the shortest possible description of the feature: you use Next.js and you want to show a You have unsaved changes that will be lost dialog when the user leaves the page. That is the entire scope, and the project is explicit that it exists because Nuxt has a Navigation Guard feature and Next.js does not. The name follows that comparison, and there is a demo hosted on GitHub Pages, plus a slide deck in English and another in Japanese that walk through how the mechanism works, the second one titled as a hack to prevent page navigation in Next.js. The MIT licence and the version of 0.3.0 in the package manifest put it in the territory of a small, focused utility rather than a framework, and the last push to the repository was on 2026-08-11.

## Installing it and wrapping the tree

The package installs the usual way.

```bash
npm install next-navigation-guard
# or
yarn install next-navigation-guard
# or
pnpm install next-navigation-guard
```

The integration is a provider that has to wrap your application, and where it goes depends on which router you use. With the App Router it goes in app/layout.tsx around the children.

```tsx
<html lang="en">
  <body className={`${geistSans.variable} ${geistMono.variable}`}>
    <NavigationGuardProvider>{children}</NavigationGuardProvider>
  </body>
</html>
```

With the Pages Router it goes in the _app file instead.

```tsx
export default function MyApp({ Component, pageProps }: AppProps) {
  return (
    <NavigationGuardProvider>
      <Component {...pageProps} />
    </NavigationGuardProvider>
  );
}
```

The peer dependencies declare next at ^14, ^15 or ^16 and react at ^18, ^19 or a 19 release candidate, so the library spans the router transitions the project has been through, and the package itself is built with tsc into a dist directory with type declarations.

## Two ways to use it: the browser prompt or your own dialog

The hook is called useNavigationGuard and it takes an enabled predicate plus an optional confirm callback. The shortest form uses the browser's own confirm dialog, which means the message is a string and the interaction is a native prompt.

```tsx
useNavigationGuard({ enabled: form.changed, confirm: () => window.confirm("You have unsaved changes that will be lost.") })
```

The second form drops the confirm callback and instead reads state off the returned object, which is what you need if you want a styled dialog. The active flag tells you whether a navigation is currently waiting on a decision, and accept and reject are the two handlers you wire to your buttons.

```tsx
const navGuard = useNavigationGuard({ enabled: form.changed })

return (
  <>
    <YourContent />

    <Dialog open={navGuard.active}>
      <DialogText>You have unsaved changes that will be lost.</DialogText>

      <DialogActions>
        <DialogButton onClick={navGuard.reject}>Cancel</DialogButton>
        <DialogButton onClick={navGuard.accept}>Discard</DialogButton>
      </DialogActions>
    </Dialog>
  </>
)
```

The two calls are semantically different and worth naming. Reject keeps the user on the current page, and accept allows the navigation to proceed and discards the work. There is a working example in the example directory with a component called NavigationGuardToggle.

## Where the guard does not reach, and the Next.js 16.2 trap

The limitations section is the most useful part of the documentation, and it is specific about three things. First, custom dialog components can only be shown for client-side navigations. When the browser fires beforeunload, on a page reload, a tab close or leaving the page, browsers do not allow asynchronous work or custom dialogs, so in those cases the library can only request the browser's built-in confirmation, and its text and appearance cannot be customized. Second, calls made directly through window.history.pushState() or window.history.replaceState() do not invoke the navigation guard at all, and if you use either method directly you have to confirm the navigation yourself before calling it. Third, and this is the sharpest edge, Next.js 16.2 has an upstream App Router regression where router.replace() that changes only the query string may be dropped after an asynchronous guard is accepted. The library says it cannot work around this without replacing the Next.js router semantics with the native History API, and that the issue is resolved in Next.js 16.3.0, so 16.1.x or 16.3.0 and later should be used when that navigation pattern is required.

## The App Router link interception, and its cost

The reason the App Router needed special handling is worth understanding, because it explains the shape of the limitations. To support App Router navigation in Next.js 15.2 and later, the provider adds a capturing-phase click handler for eligible internal links. While a guard is enabled, that handler prevents the original click and stops its propagation while the confirmation is pending. The documented consequence is that code depending on the click's normal propagation order may need to account for it. This is the standard trade-off of intercepting navigation at the DOM level rather than through the router, and it is the reason the Pages Router path is the smoother one. If your application has a click handler somewhere that expects to fire before a route change, a capturing handler in the document will see the event first, and the library is telling you so up front rather than leaving you to find out.

## Testing it, and the two switches that matter

The library has a testing story that is unusually well thought through, and it rests on two props. The first is disableForTesting, which makes the library skip its host-environment hooks, so no popstate, beforeunload or click listeners are attached and window.history is not augmented. What remains in place are the App Router and Pages Router context overrides, so navigation through the Next.js routers, including mock routers in tests, still evaluates registered guards. useNavigationGuard keeps registering normally, so the enabled predicate, the confirm callback and the active, accept and reject values all work as they do in production.

```tsx
<NavigationGuardProvider disableForTesting>
  <MyComponent />
</NavigationGuardProvider>
```

The second is mockConfirm, which bypasses the confirmation UI entirely and replaces every registered guard's confirm during evaluation, while the per-guard enabled predicates still run.

```tsx
const mockConfirm = jest.fn().mockResolvedValue(true);
render(
  <NavigationGuardProvider disableForTesting={{ mockConfirm }}>
    <MyComponent />
  </NavigationGuardProvider>
);
// drive navigation through your mock router…
expect(mockConfirm).toHaveBeenCalledWith({ to: "/foo", type: "push" });
```

The assertion is worth noting, because it tells you what the confirm callback receives, a navigation description with a to path and a type of push. The project also ships end-to-end tests through Playwright, with the e2e script and a ui mode, and a check:packaging script that runs the arethetypeswrong CLI over the package, which is the tool you want when the packaging of a TypeScript library is wrong in ways unit tests never notice.

## Conclusion

next-navigation-guard fits a Next.js team that ships forms with unsaved state and needs a confirmation before a route change, since the library covers both the App Router and the Pages Router and lets you supply either the browser's own confirm or a styled dialog. It does not fit an application that navigates through window.history.pushState or replaceState directly, because those calls bypass the guard entirely and you have to confirm them yourself, and it does not fit a Next.js 16.2 user who relies on query-only router.replace calls after an accepted guard, because the library documents that upstream regression and recommends 16.1.x or 16.3.0 and later. Before you ship it, read the limitations section, and if you use custom dialogs, test the browser-controlled navigations separately since those can only produce the browser's own prompt.

## FAQ

### How do I add an unsaved-changes prompt to a Next.js app?

Install next-navigation-guard, wrap your tree in NavigationGuardProvider, in app/layout.tsx for the App Router or the _app file for the Pages Router, then call useNavigationGuard with an enabled predicate and either a confirm callback using window.confirm or your own dialog driven by the active, accept and reject values.

### Can next-navigation-guard show a custom dialog when the user closes the tab?

No. Custom dialog components can only be shown for client-side navigations. When the browser fires beforeunload, on reload, tab close or leaving the page, browsers do not allow asynchronous work or custom dialogs, so the library can only request the browser's built-in confirmation and its text and appearance cannot be customized.

### Does next-navigation-guard intercept window.history.pushState calls?

No. Calls made directly through window.history.pushState() or window.history.replaceState() do not invoke the navigation guard, and if you use either method directly you have to confirm the navigation yourself before calling it.

### Which Next.js versions should I use with next-navigation-guard?

The peer dependency allows next ^14, ^15 or ^16. The project warns that Next.js 16.2 has an upstream App Router regression where query-only router.replace calls may be dropped after an accepted guard, and recommends Next.js 16.1.x or 16.3.0 and later when that navigation pattern is required.

## Sources

- [Issues](https://github.com/LayerXcom/next-navigation-guard/issues)
- [LayerXcom/next-navigation-guard on GitHub](https://github.com/LayerXcom/next-navigation-guard)
- [License: MIT](https://github.com/LayerXcom/next-navigation-guard/blob/main/LICENSE)
- [README](https://github.com/LayerXcom/next-navigation-guard/blob/main/README.md)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/layerxcom-next-navigation-guard
