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, GDPR-compliant by default. Start your free trial or view a live demo.