next-navigation-guard: an unsaved-changes prompt for Next.js
Cancel page navigation in Next.js, like Nuxt's Navigation Guard feature.
At a glance
- What is it?
- 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.
- Who is it for?
- 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.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 73 days ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
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.
npm install next-navigation-guard
# or
yarn install next-navigation-guard
# or
pnpm install next-navigation-guardThe 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.
<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.
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.
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.
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.
<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.
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.
Editorial 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.
Frequently asked questions
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.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/layerxcom-next-navigation-guard)Was this useful?