Popups
The website's popups (Popups in the dashboard, manage_popups), with the Panda website behavior: page targeting (/, /*, /listing/*, exact paths; a leading locale is ignored), trigger (delay in seconds, scroll depth, exit…
Anatomy
Every piece of Popups as it nests in code. A root holds the data and state; its parts and hooks only work inside it.
<PandaProvider>Mounted once in app/layout.tsx<PandaPopups />complete widgetThe website's popups (Popups in the dashboard, manage_popups), with the Panda website behavior: page targeting (/, /*, /listing/*, exact…
<PandaPopupsRoot>Holds the data and state its parts and hooks read<PopupFrame />The Panda popup: auth popups open the shared sign-up dialog, form popups the form in a dialog (classNames).
<PopupForm />The lead form of the form popup on screen (attributed to the popup; a lead closes it).
<PopupAuth />The sign-up / log-in flow of the auth popup on screen, inline (attributed to the popup).
<PandaPopupsDesign />Your popup design (render, defined at module level in a client file) for the PandaPopups the managed layout already…
</PandaPopupsRoot></PandaProvider>Installation
- 1npm install <tarball or @pandaidx/widgets>Terminalnpm install https://github.com/pandaidx/panda-widgets/releases/download/v1.14.1/pandaidx-widgets-1.14.1.tgz
- 2import "@pandaidx/widgets/styles.css" in app/layout.tsx
- 3Wrap the site in <PandaProvider publicKey apiUrl preset theme> (app/layout.tsx)
- 4In each page (Server Component) call the widget's server loader, then render the widget with its data
- 5Allow the images: next.config images.remotePatterns [{ protocol: "https", hostname: "**" }] (listing and blog photos come from several CDNs)
Usage
// app/layout.tsx: once in the shell, inside the providerimport { PandaPopups, PandaProvider } from "@pandaidx/widgets/client"import { fetchPopups } from "@pandaidx/widgets/server"export default async function RootLayout({ children }: { children: React.ReactNode }) {const popups = await fetchPopups({ publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL })return (<html lang="en"><body><PandaProvider publicKey={process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!} apiUrl={process.env.NEXT_PUBLIC_PANDA_API_URL}>{children}<PandaPopups popups={popups} /></PandaProvider></body></html>)}
Examples
Your own popup design (bottom card), Panda form and sign-up inside
// components/site/popup-design.tsx (a client file; the managed layout already mounts <PandaPopups />)"use client"import { PandaPopupsDesign, PopupForm, PopupAuth, type WidgetPopup } from "@pandaidx/widgets/client"// Module level: one stable render for the whole site.const renderPopup = (popup: WidgetPopup, { dismiss }: { dismiss: () => void }) => (<aside role="dialog" aria-label={popup.form?.name ?? "Join"} className="fixed inset-x-4 bottom-4 z-50 max-w-md rounded-2xl bg-background p-6 shadow-xl md:inset-x-auto md:right-6">{popup.dismissable ? <button type="button" aria-label="Close" onClick={dismiss}>×</button> : null}<PopupForm /><PopupAuth /></aside>)export const PopupDesign = () => <PandaPopupsDesign render={renderPopup} />// app/site-shell.tsx: render <PopupDesign /> once
One popup with its own design (a contact sheet), the others with the shared one
// components/site/contact-popup.tsx ("use client"): the popup `contact` (manage_popups: type form, formSpot contact, trigger link /contact)"use client"import { PandaPopupsDesign, PopupForm, type WidgetPopup } from "@pandaidx/widgets/client"const renderContact = (_popup: WidgetPopup, { dismiss }: { dismiss: () => void }) => (<div role="dialog" aria-label="Contact" className="fixed inset-y-0 right-0 z-50 w-full max-w-lg bg-background p-8 shadow-2xl"><button type="button" aria-label="Close" onClick={dismiss}>×</button><PopupForm /></div>)export const ContactPopup = () => <PandaPopupsDesign popup="contact" render={renderContact} />// app/site-shell.tsx: render <ContactPopup /> once; every link to /contact now opens it
Open a popup from your own button
// Any element, server or client: its key comes from manage_popups list (its pages still apply).<button type="button" data-panda-popup="guide">Get the guide</button>
API reference
PandaPopups
The website's popups (Popups in the dashboard, manage_popups), with the Panda website behavior: page targeting (/, /*, /listing/*, exact paths; a leading locale is ignored), trigger (delay in seconds, scroll depth, exit intent, a click on a selector, every link to a site path such as /contact, after N different listings opened (views, the registration wall: a sign-up popup that is not dismissable), or manual: only the site's buttons), a stable key per popup (contact) that opens it from any element with data-panda-popup="<key>" or openPandaPopup(key), frequency for the popups that open by themselves (always, once_per_session in sessionStorage, once in localStorage, counted when the visitor sees it) and non-dismissable popups. Every dialog that opens over the page is one of these records, never a hand-made dialog with its own click listener. Auth popups open the shared Panda sign-up / log-in dialog (full screen); form popups render the tenant form (PandaForm) in a dialog and close after the submit. Signed-in visitors never see popups. Mount it ONCE in the site shell (app/layout.tsx), inside <PandaProvider>; it renders nothing until a popup matches. Never submit its form from automated tests: each submit creates a real contact.
Import from @pandaidx/widgets/client · renders inside PandaProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| popups | array | — | Enabled popups from the server loader. Omit it to load GET /v1/popups in the browser. |
| classNames | object | — | Host classes appended to the form popup slots: dialog, title, description. |
| preset | "default" | "template1" | — | Preset for this widget only (defaults to the provider preset). |
| variant | "default" | "template1" | — | Deprecated alias of preset. |
| theme | object | — | Token overrides for this widget only, merged over the provider theme (same shape as PandaProvider.theme). |
| render | function | — | Your own popup surface instead of the Panda dialog: (popup, { dismiss, complete }) => <MyModal>…<PopupForm /></MyModal> (popup.key, popup.type form | auth | content, popup.form, popup.trigger); PopupForm / PopupAuth render the Panda lead form / sign-up inside it. |
Parts
<PopupFrame />The Panda popup: auth popups open the shared sign-up dialog, form popups the form in a dialog (classNames).
<PopupForm />The lead form of the form popup on screen (attributed to the popup; a lead closes it).
<PopupAuth />The sign-up / log-in flow of the auth popup on screen, inline (attributed to the popup).
<PandaPopupsDesign />Your popup design (render, defined at module level in a client file) for the PandaPopups the managed layout already mounts: render it in app/site-shell.tsx, once for every popup, or once per popup with popup="<key>" (its own design; the one without popup draws the others).
Hooks
usePandaPopups()inside PandaPopupsRoot{ active: WidgetPopup | null ({ id, key, type: "form" | "auth" | "content", mode, form, pages, delay, trigger: { kind: "delay" | "scroll" (percent) | "exit" | "click" (selector) | "link" (href) | "manual" }, frequency, dismissable }), dismiss(), complete(), trigger(idOrKey) } — your own popup surface; data-panda-popup="<key>" opens a popup from any element
Server data
Loaders run in a Server Component and come from @pandaidx/widgets/server. Typed data is for a page you design yourself, rendered inside the root.
fetchPopups()widget loaderThe tenant's enabled popups with their targeting (GET /v1/popups); [] when none. Returns WidgetPopup[] → popups
fetchPopups()typed dataThe website's enabled popups (dashboard → Popups) with their key, trigger, pages and frequency: the popups prop of <PandaPopups>. Open one from a button with data-panda-popup="<key>". Never submit a popup form yourself.