Embed Forms in Next.js
This guide covers embedding a RocketLead form in a Next.js app using the imperative window.rocketlead.forms.create() API. The widget’s auto-mount (<script async> + [data-rocketlead-form]) doesn’t work cleanly in client-routed apps — you need to wire mount/unmount into your component’s lifecycle.
If you’re new to the widget, skim the SPA Frameworks reference first — this guide is the Next.js-specific application of that.
Prerequisites
Section titled “Prerequisites”- A Next.js app (App Router or Pages Router — both covered below).
- A RocketLead form that’s published. Drafts return
404from the public API. - The form’s ID — copy it from the form’s embed dialog.
App Router
Section titled “App Router”1. Load the script and stylesheet
Section titled “1. Load the script and stylesheet”In app/layout.tsx:
import Script from 'next/script';
export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> <link rel="preload" href="https://cdn.rocketlead.io/widget.css" as="style" /> <link rel="stylesheet" href="https://cdn.rocketlead.io/widget.css" /> </head> <body> {children} <Script src="https://cdn.rocketlead.io/static/forms/widget.js" strategy="afterInteractive" /> </body> </html> );}strategy="afterInteractive" loads the script after hydration — safe and consistent across navigations.
2. Mount the form in a Client Component
Section titled “2. Mount the form in a Client Component”'use client';
import { useEffect, useRef } from 'react';
interface Props { formId: string;}
export function RocketLeadForm({ formId }: Props) { const ref = useRef<HTMLDivElement>(null);
useEffect(() => { if (!ref.current) return; const handle = window.rocketlead?.forms?.create({ target: ref.current, formId, }); return () => handle?.unmount(); }, [formId]);
return <div ref={ref} />;}3. Use it in a page
Section titled “3. Use it in a page”import { RocketLeadForm } from './RocketLeadForm';
export default function ContactPage() { return ( <main> <h1>Get in touch</h1> <RocketLeadForm formId="your-form-id" /> </main> );}That’s it. The form mounts on entry, unmounts on navigation away (or component unmount), and re-mounts cleanly on return.
Pages Router
Section titled “Pages Router”1. Load the script and stylesheet
Section titled “1. Load the script and stylesheet”In pages/_document.tsx:
import { Html, Head, Main, NextScript } from 'next/document';
export default function Document() { return ( <Html> <Head> <link rel="preload" href="https://cdn.rocketlead.io/widget.css" as="style" /> <link rel="stylesheet" href="https://cdn.rocketlead.io/widget.css" /> </Head> <body> <Main /> <NextScript /> <script src="https://cdn.rocketlead.io/static/forms/widget.js" defer></script> </body> </Html> );}defer is fine here — the document-level script loads once and survives client-side route changes.
2. Mount the form in a component
Section titled “2. Mount the form in a component”Same as App Router — the component code is unchanged:
import { useEffect, useRef } from 'react';
export function RocketLeadForm({ formId }: { formId: string }) { const ref = useRef<HTMLDivElement>(null);
useEffect(() => { if (!ref.current) return; const handle = window.rocketlead?.forms?.create({ target: ref.current, formId, }); return () => handle?.unmount(); }, [formId]);
return <div ref={ref} />;}Use it in any page (pages/contact.tsx):
import { RocketLeadForm } from '@/components/RocketLeadForm';
export default function ContactPage() { return ( <main> <h1>Get in touch</h1> <RocketLeadForm formId="your-form-id" /> </main> );}TypeScript types
Section titled “TypeScript types”Add a global ambient declaration so window.rocketlead is typed:
// types/rocketlead.d.ts (or anywhere in your project)interface RocketLeadFormHandle { unmount: () => void;}
interface RocketLeadCreateOptions { target: HTMLElement | string; formId: string; prefill?: Record<string, string | string[]>; hideFields?: string[];}
declare global { interface Window { rocketlead?: { forms?: { create(options: RocketLeadCreateOptions): RocketLeadFormHandle; }; }; }}
export {};Make sure the file is included in your tsconfig.json’s include array (it usually is by default if it’s under types/ or src/).
Prefill and hideFields
Section titled “Prefill and hideFields”Pass UTM parameters or any pre-known data from the URL straight into the form:
'use client';import { useEffect, useRef } from 'react';import { useSearchParams } from 'next/navigation';
export function RocketLeadForm({ formId }: { formId: string }) { const ref = useRef<HTMLDivElement>(null); const params = useSearchParams();
useEffect(() => { if (!ref.current) return; const handle = window.rocketlead?.forms?.create({ target: ref.current, formId, prefill: { utm_source: params.get('utm_source') ?? 'direct', utm_campaign: params.get('utm_campaign') ?? '', }, hideFields: ['utm_source', 'utm_campaign'], }); return () => handle?.unmount(); }, [formId, params]);
return <div ref={ref} />;}The user sees a clean form; the submission carries the UTM context. See Prefill cascade for the full precedence rules and Stable Field Names for how prefill keys resolve.
Listening to events
Section titled “Listening to events”Form events fire as DOM CustomEvents on document. Set up listeners in a Client Component (App Router) or a Pages Router page:
'use client';import { useEffect } from 'react';
export function FormAnalytics() { useEffect(() => { const onSuccess = (e: Event) => { const detail = (e as CustomEvent).detail; console.log('Submitted:', detail.submissionId); // fbq('track', 'Lead', { eventID: detail.submissionId }); }; document.addEventListener('rl:form:submit:success', onSuccess); return () => document.removeEventListener('rl:form:submit:success', onSuccess); }, []);
return null;}See the full event catalog and the ad-pixel pattern using submissionId.
Common gotchas
Section titled “Common gotchas””window is not defined”
Section titled “”window is not defined””Don’t call window.rocketlead.forms.create() at the top level of a module — it’ll fail during SSR. Always call it inside useEffect, which only runs on the client.
Hot reload duplicates the form
Section titled “Hot reload duplicates the form”create() is idempotent on the same target — a second call unmounts the previous instance first. With React Strict Mode (default in Next.js dev), useEffect runs twice on mount; the cleanup-then-remount cycle handles this correctly. If you see duplicate forms, your useEffect dependencies probably aren’t stable — make sure formId is a string literal or useMemo’d, not a fresh object each render.
Form is empty after first navigation
Section titled “Form is empty after first navigation”You’re using the auto-mount pattern (<div data-rocketlead-form>) instead of create(). The auto-scan only runs once on DOMContentLoaded — re-entering the route won’t re-mount. Switch to the create() pattern shown above.
Script loads before the component renders
Section titled “Script loads before the component renders”window.rocketlead is undefined at first render. The optional chaining (window.rocketlead?.forms?.create) handles this gracefully — useEffect runs after the script finishes loading on subsequent navigations, so subsequent mounts work. If the FIRST mount is consistently empty, increase the script’s load priority (strategy="beforeInteractive" in App Router) or check the network tab for failed widget.js loads.
Multiple forms on one page
Section titled “Multiple forms on one page”No special handling needed. Each <RocketLeadForm formId="..."> instance manages its own target ref, and the widget tracks instances in a WeakMap. Just render as many as you need:
<RocketLeadForm formId="form-a" /><RocketLeadForm formId="form-b" />Strict CSP / nonce
Section titled “Strict CSP / nonce”If your Next.js app uses a strict Content Security Policy, you need to allow cdn.rocketlead.io for script-src and style-src, and api.rocketlead.io (or whatever your form API host is) for connect-src.
Allowed Domains
Section titled “Allowed Domains”If your form has Erlaubte Domains configured in Form Settings, add your Next.js production domain (and any preview URLs) to the list. Browser submissions from disallowed origins return 403.
Related
Section titled “Related”- Forms → SPA Frameworks — full
create()reference, idempotency rules. - Prefill cascade — sources, precedence, and pinning.
- Stable Field Names — how
prefillkeys resolve to fields. - Form Events — DOM events, dataLayer, ad-pixel integration.
- WordPress + Elementor — different stack, same widget.