Next.js
Next.js ships its own <Script> component that controls when third-party scripts load relative to hydration. Use it for Statable in both the App Router (app/, Next.js 13+) and the Pages Router (pages/). The component goes into a single shared layout, never into individual pages, so it loads once per session and survives client-side route transitions.
Statable hooks the History API, so route changes from next/link and useRouter are tracked automatically. No manual pageview wiring required.
Compatible with Next.js 13, 14, 15, and 16. The
next/scriptAPI is stable across all of these versions.
Install
App Router (Next.js 13+)
Add <Script> to the root app/layout.tsx. The component is a Server Component by default. Place it as a sibling of <body> so Next.js can manage injection. The default strategy is afterInteractive, which is the recommended choice for analytics: the script loads early, but after first-party hydration starts.
// app/layout.tsx
import Script from 'next/script'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
<Script
src={`https://statable.com/js/${process.env.NEXT_PUBLIC_STATABLE_ID}/s.js`}
strategy="afterInteractive"
/>
</html>
)
}
Set NEXT_PUBLIC_STATABLE_ID in .env.local:
The NEXT_PUBLIC_ prefix is required. Without it, Next.js keeps the value server-side and src resolves to https://statable.com/js/undefined/s.js in the browser bundle.
Pages Router
For the Pages Router, put <Script> in pages/_app.tsx. This is the recommended placement for afterInteractive scripts that load on every route. Do not use pages/_document.tsx for this. The Pages Router only allows <Script> in _document.tsx when the strategy is beforeInteractive.
// pages/_app.tsx
import type { AppProps } from 'next/app'
import Script from 'next/script'
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Script
src={`https://statable.com/js/${process.env.NEXT_PUBLIC_STATABLE_ID}/s.js`}
strategy="afterInteractive"
/>
<Component {...pageProps} />
</>
)
}
Strategy choice
next/script exposes four strategies. For Statable, pick afterInteractive:
| Strategy | When it loads | Use for |
|---|---|---|
beforeInteractive | Before any Next.js code, in the initial HTML | Bot detection, consent managers |
afterInteractive | Default. After hydration starts | Analytics, tag managers (Statable) |
lazyOnload | Browser idle time, after every other resource | Chat widgets, social embeds |
worker | Web Worker via Partytown. Experimental, Pages only | Advanced offload, not needed for Statable |
afterInteractive is the default, so the strategy prop is technically optional, but stating it explicitly documents intent.
Tracking custom events
Fire events from any Client Component using window.statable.t(). Add a TypeScript declaration so the global is typed:
// types/global.d.ts
declare global {
interface Window {
statable: {
t: (event: string, props?: Record<string, unknown>) => void
}
}
}
export {}
Then call it from a Client Component. The 'use client' directive is required because window.statable only exists in the browser:
'use client'
import { useEffect } from 'react'
export function PricingHero() {
useEffect(() => {
window.statable?.t?.('Pricing Page Loaded', { variant: 'A' })
}, [])
return <button onClick={() => window.statable?.t?.('CTA Clicked')}>Start free</button>
}
The optional chaining (window.statable?.t?.(...)) silently no-ops if the script hasn't loaded yet — safe to call during hydration.
Tracking page views in SPA
Next.js triggers history.pushState on every client-side route change. Statable listens and fires a pageview automatically. No router subscription, no useEffect on usePathname(), no manual call.
Excluding internal traffic
Statable respects a per-browser opt-out flag in localStorage. To exclude your own dev visits, run this once in DevTools on the live site:
Remove the flag with localStorage.removeItem('analytics_ignore'). Full procedure in Verify installation.
Verify it's working
- Open the Statable dashboard, go to Realtime, load any page on your site.
- The session appears within a few seconds.
- See Verify installation for the full checklist.
Common pitfalls
- Putting the script in a single page instead of
app/layout.tsxorpages/_app.tsx. It will reload on each navigation and double-count sessions. Always go through the shared layout. - Using
pages/_document.tsxwithafterInteractive. The Pages Router rejects this combination. Either move the<Script>to_app.tsx(recommended), or switch the strategy tobeforeInteractive, which is reserved for critical scripts. - Calling
window.statablefrom a Server Component. It only exists in the browser. Mark any component that calls it with'use client'. - Forgetting
NEXT_PUBLIC_on the env var. Without that prefix, Next.js does not expose the value to the browser bundle andsrcresolves tohttps://statable.com/js/undefined/s.js. - Wrapping
<Script>in a Client Component just to useonLoad. You only need'use client'if you actually pass anonLoad,onReady, oronErrorhandler. The basic Statable install does not.
See also: Install the tracking script, Custom events, JavaScript API.
Ready to take control of your web analytics? Try Statable free for 30 days. No credit card required, full feature access, built for GDPR. Start your free trial or view a live demo.

