` wrapper to detect
+ navigation start.
+- **`usePathname()` / `useSearchParams()`** to detect navigation completion, as
+ recommended by the [Next.js
+ docs](https://nextjs.org/docs/app/api-reference/functions/use-router#router-events).
+
+To run it:
+
+```
+$ npm i && npm run dev
+```
+
+Then open [http://localhost:3000](http://localhost:3000) to view it in the
+browser.
diff --git a/examples/next-app-router/app/about/page.tsx b/examples/next-app-router/app/about/page.tsx
new file mode 100644
index 000000000..f873e99b4
--- /dev/null
+++ b/examples/next-app-router/app/about/page.tsx
@@ -0,0 +1,7 @@
+export default async function AboutPage() {
+ await new Promise((resolve) => {
+ setTimeout(resolve, 500)
+ })
+
+ return This is about Next.js!
+}
diff --git a/examples/next-app-router/app/forever/page.tsx b/examples/next-app-router/app/forever/page.tsx
new file mode 100644
index 000000000..7a0769afb
--- /dev/null
+++ b/examples/next-app-router/app/forever/page.tsx
@@ -0,0 +1,7 @@
+export default async function ForeverPage() {
+ await new Promise((resolve) => {
+ setTimeout(resolve, 3000)
+ })
+
+ return This page was rendered for a while!
+}
diff --git a/examples/next-app-router/app/layout.tsx b/examples/next-app-router/app/layout.tsx
new file mode 100644
index 000000000..86612ddde
--- /dev/null
+++ b/examples/next-app-router/app/layout.tsx
@@ -0,0 +1,30 @@
+import NavigationProgress from '../components/NavigationProgress'
+import ProgressLink from '../components/ProgressLink'
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ return (
+
+
+
+
+ {children}
+
+
+
+ )
+}
diff --git a/examples/next-app-router/app/page.tsx b/examples/next-app-router/app/page.tsx
new file mode 100644
index 000000000..fc4702faa
--- /dev/null
+++ b/examples/next-app-router/app/page.tsx
@@ -0,0 +1,3 @@
+export default function HomePage() {
+ return Hello Next.js!
+}
diff --git a/examples/next-app-router/components/Loading.tsx b/examples/next-app-router/components/Loading.tsx
new file mode 100644
index 000000000..3b73c2389
--- /dev/null
+++ b/examples/next-app-router/components/Loading.tsx
@@ -0,0 +1,50 @@
+'use client'
+
+import { useNProgress } from '@tanem/react-nprogress'
+
+const Loading: React.FC<{ isRouteChanging: boolean }> = ({
+ isRouteChanging,
+}) => {
+ const { animationDuration, isFinished, progress } = useNProgress({
+ isAnimating: isRouteChanging,
+ })
+
+ return (
+
+ )
+}
+
+export default Loading
diff --git a/examples/next-app-router/components/NavigationProgress.tsx b/examples/next-app-router/components/NavigationProgress.tsx
new file mode 100644
index 000000000..9a74941d3
--- /dev/null
+++ b/examples/next-app-router/components/NavigationProgress.tsx
@@ -0,0 +1,82 @@
+'use client'
+
+import { usePathname, useSearchParams } from 'next/navigation'
+import {
+ createContext,
+ Suspense,
+ useCallback,
+ useContext,
+ useEffect,
+ useRef,
+ useState,
+} from 'react'
+
+import Loading from './Loading'
+
+type NavigationProgressContextType = {
+ start(): void
+}
+
+const NavigationProgressContext =
+ createContext(null)
+
+export function useNavigationProgress() {
+ const context = useContext(NavigationProgressContext)
+ if (!context) {
+ throw new Error(
+ 'useNavigationProgress must be used within ',
+ )
+ }
+ return context
+}
+
+// Watches pathname/searchParams changes to detect when
+// navigation has completed. Wrapped in Suspense because
+// useSearchParams() requires a Suspense boundary.
+function NavigationComplete({ onComplete }: { onComplete: () => void }) {
+ const pathname = usePathname()
+ const searchParams = useSearchParams()
+ const currentUrl = useRef(pathname + searchParams.toString())
+
+ useEffect(() => {
+ const newUrl = pathname + searchParams.toString()
+ if (newUrl !== currentUrl.current) {
+ currentUrl.current = newUrl
+ onComplete()
+ }
+ }, [pathname, searchParams, onComplete])
+
+ return null
+}
+
+// Provides navigation progress state to the component
+// tree. Navigation start is signalled via the onNavigate
+// prop on a , and completion is detected by
+// watching usePathname()/useSearchParams().
+export default function NavigationProgress({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const [isRouteChanging, setIsRouteChanging] = useState(false)
+ const [loadingKey, setLoadingKey] = useState(0)
+
+ const contextValue = useRef({
+ start: () => {
+ setIsRouteChanging(true)
+ setLoadingKey((prev) => prev ^ 1)
+ },
+ }).current
+
+ const handleComplete = useCallback(() => setIsRouteChanging(false), [])
+
+ return (
+
+
+
+
+
+ {children}
+
+ )
+}
diff --git a/examples/next-app-router/components/ProgressLink.tsx b/examples/next-app-router/components/ProgressLink.tsx
new file mode 100644
index 000000000..a10577ff6
--- /dev/null
+++ b/examples/next-app-router/components/ProgressLink.tsx
@@ -0,0 +1,22 @@
+'use client'
+
+import Link from 'next/link'
+
+import { useNavigationProgress } from './NavigationProgress'
+
+// A thin wrapper around next/link that signals navigation start via the
+// onNavigate callback introduced in Next.js 15.3. Use this in place of
+// wherever you want the progress bar to appear during navigation.
+export default function ProgressLink(props: React.ComponentProps) {
+ const { start } = useNavigationProgress()
+
+ return (
+ {
+ start()
+ props.onNavigate?.(e)
+ }}
+ />
+ )
+}
diff --git a/examples/next-app-router/next-env.d.ts b/examples/next-app-router/next-env.d.ts
new file mode 100644
index 000000000..1b3be0840
--- /dev/null
+++ b/examples/next-app-router/next-env.d.ts
@@ -0,0 +1,5 @@
+///
+///
+
+// NOTE: This file should not be edited
+// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
diff --git a/examples/next-app-router/package.json b/examples/next-app-router/package.json
new file mode 100644
index 000000000..959e6c354
--- /dev/null
+++ b/examples/next-app-router/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "next-app-router",
+ "description": "ReactNProgress Next App Router Example",
+ "keywords": [
+ "@tanem/react-nprogress"
+ ],
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "next dev",
+ "build": "next build",
+ "start": "next start"
+ },
+ "dependencies": {
+ "@tanem/react-nprogress": "latest",
+ "next": "latest",
+ "react": "19.2.4",
+ "react-dom": "19.2.4"
+ },
+ "devDependencies": {
+ "@types/node": "24.10.13",
+ "@types/react": "19.2.14",
+ "typescript": "5.9.3"
+ }
+}
diff --git a/examples/next-app-router/tsconfig.json b/examples/next-app-router/tsconfig.json
new file mode 100644
index 000000000..abe252433
--- /dev/null
+++ b/examples/next-app-router/tsconfig.json
@@ -0,0 +1,42 @@
+{
+ "compilerOptions": {
+ "target": "es5",
+ "lib": [
+ "dom",
+ "dom.iterable",
+ "esnext"
+ ],
+ "allowJs": true,
+ "skipLibCheck": true,
+ "strict": true,
+ "forceConsistentCasingInFileNames": true,
+ "noEmit": true,
+ "esModuleInterop": true,
+ "module": "esnext",
+ "moduleResolution": "bundler",
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "jsx": "react-jsx",
+ "incremental": true,
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ],
+ "paths": {
+ "@/*": [
+ "./*"
+ ]
+ }
+ },
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts",
+ ".next/dev/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}