- Blog
- How to Add a Waitlist to React & Next.js
Article
How to Add a Waitlist to React & Next.js
Embed a GetWaitly waitlist in React or Next.js — load one script in your layout and render a container div. App Router, Pages Router, and plain React examples.
If you are building your pre-launch site in React or Next.js, you do not need a waitlist SDK or npm package. GetWaitly ships a single script tag and a container element — the same embed that works on any site. Here is how to wire it into a React or Next.js app cleanly.
The short answer
Load https://getwaitly.com/widget.js once in your root layout, then render a <div className="gw-widget" data-waitlist-id="YOUR_WAITLIST_ID" /> wherever the form should appear. For a popup, render a <button className="gw-trigger" data-waitlist-id="YOUR_WAITLIST_ID">. Use the waitlist's public ID, not its slug. Total time: 5 minutes.
What you need
- A GetWaitly account with a waitlist created (free plan covers 100 subscribers).
- Your waitlist's public ID from the Widget/Embed section of the dashboard.
- A React or Next.js app. The widget is plain JavaScript — it does not care which framework you use.
The embed, in framework terms
The GetWaitly widget is two parts: a script that defines the <div class="gw-widget"> behavior, and the div itself. In JSX:
<div className="gw-widget" data-waitlist-id="YOUR_WAITLIST_ID" />
The script must load once per page. In Next.js that is next/script; in plain React you can put it in index.html or inject it in an effect. The widget finds the .gw-widget element and renders the form into it, styled with your waitlist branding.
Next.js (App Router)
Load the script in your root layout so it is present on every route, and render the widget on the page that needs it.
app/layout.tsx
import Script from "next/script";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Script
src="https://getwaitly.com/widget.js"
strategy="afterInteractive"
/>
</body>
</html>
);
}
app/page.tsx (or any page)
export default function Home() {
return (
<main>
<h1>Something great is coming.</h1>
<div className="gw-widget" data-waitlist-id="YOUR_WAITLIST_ID" />
</main>
);
}
strategy="afterInteractive" loads the script after the page becomes interactive, so it does not block your render.
Next.js (Pages Router)
Use _app.tsx to load the script once, and place the div in your page:
pages/_app.tsx
import Script from "next/script";
import type { AppProps } from "next/app";
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Component {...pageProps} />
<Script
src="https://getwaitly.com/widget.js"
strategy="afterInteractive"
/>
</>
);
}
Plain React (Vite / Create React App)
There is no next/script, so add the script tag to index.html before the closing </body>, and render the div in a component:
index.html
<body>
<div id="root"></div>
<script src="https://getwaitly.com/widget.js" defer></script>
</body>
src/App.tsx
export default function App() {
return (
<main>
<h1>Join the waitlist</h1>
<div className="gw-widget" data-waitlist-id="YOUR_WAITLIST_ID" />
</main>
);
}
If you cannot edit index.html, inject the script in an effect instead:
import { useEffect } from "react";
useEffect(() => {
if (document.querySelector('script[src="https://getwaitly.com/widget.js"]')) return;
const s = document.createElement("script");
s.src = "https://getwaitly.com/widget.js";
s.defer = true;
document.body.appendChild(s);
}, []);
The popup variant
For a button that opens the form in a modal, load the script once (above) and render a trigger button:
<button className="gw-trigger" data-waitlist-id="YOUR_WAITLIST_ID">
Join waitlist
</button>
TypeScript and custom fields
data-waitlist-id is a valid data-* attribute on a div and button in JSX, no type changes needed. Enable name, phone, or custom fields in your GetWaitly widget settings and the form renders them automatically.
Single-page apps: one caveat
The widget script initializes on page load. In a single-page app the script loads once and does not re-run on client-side navigation. In practice this is fine because the widget renders into the .gw-widget div wherever it appears. If you mount the div conditionally after the script has loaded, the widget may miss it on the first client-side transition — a full page load fixes it. If that matters for your flow, keep the container div mounted, or drive the form yourself with the no-JavaScript form action below.
Full control: post to the join endpoint
If you would rather build your own React form (for custom validation or styling), skip the widget script and POST to your waitlist's join endpoint:
<form action="https://getwaitly.com/api/join/YOUR_WAITLIST_ID" method="post">
<input type="email" name="email" required placeholder="Your email" />
<button type="submit">Join the waitlist</button>
</form>
The browser posts the signup and redirects to your thank-you page. You lose the inline success animation and the automatic branding, but you keep full control of the markup.
Testing
Run your app, open the page, and submit a test email. Confirm the success message appears and the subscriber lands in your GetWaitly dashboard. If the form does not render, check the browser console for a blocked script (ad blockers and strict CSP can block third-party scripts) and verify the data-waitlist-id exactly matches your waitlist.
FAQ
Is there an npm package for the GetWaitly widget?
No. You inject one script tag (https://getwaitly.com/widget.js) and render a container div. There is nothing to install.
Does GetWaitly work with Next.js Server Components?
Yes — the script loads via next/script in your layout, and the container div is a plain element you can render from a Server Component. If you add the script in an effect, put that in a Client Component.
Why does the widget not render after client-side navigation?
The script initializes on load and does not re-run on route changes. Keep the .gw-widget div mounted, or use the form action to POST to /api/join yourself. A hard refresh always re-initializes.
Can I use the popup trigger in React?
Yes. Render a <button className="gw-trigger" data-waitlist-id="YOUR_WAITLIST_ID">. Load the script once and any trigger on the page opens the modal.
Does it work with Vite, Remix, or Astro?
Yes. Anywhere you can add a <script> tag and render a div, the widget works. For server-rendered frameworks, load the script with whatever mechanism the framework recommends (or a plain defer tag).
How do I collect custom fields?
Configure them in your GetWaitly widget settings. The inline widget renders the fields you enable. For a fully custom form, use cf_0, cf_1, … inputs matched to your custom field order and post to /api/join.
Related guides
- How to Add a Waitlist to Any Website — the generic HTML embed
- How to Add a Waitlist to Framer — the Embed component method
- Which Form Builder for a Waitlist? — form builders vs a dedicated waitlist tool
- 7 Best Waitlist Tools for Solo Founders in 2026 — the full comparison
The simple, affordable waitlist for indie hackers. Start free in 5 minutes — no credit card required. Try GetWaitly free →
Written by
Aymane E.
Founder of GetWaitly. I write about validating ideas, pre-launch, and growth from building a waitlist tool as a solo founder.
Follow on X →