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…

Fully headlessClient component@pandaidx/widgets 1.14.1

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 widget

The 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

  1. 1
    npm install <tarball or @pandaidx/widgets>
    Terminal
    npm install https://github.com/pandaidx/panda-widgets/releases/download/v1.14.1/pandaidx-widgets-1.14.1.tgz
  2. 2
    import "@pandaidx/widgets/styles.css" in app/layout.tsx
  3. 3
    Wrap the site in <PandaProvider publicKey apiUrl preset theme> (app/layout.tsx)
  4. 4
    In each page (Server Component) call the widget's server loader, then render the widget with its data
  5. 5
    Allow the images: next.config images.remotePatterns [{ protocol: "https", hostname: "**" }] (listing and blog photos come from several CDNs)

Usage

PandaPopups
// app/layout.tsx: once in the shell, inside the provider
import { 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

PandaPopups
// 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

PandaPopups
// 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

PandaPopups
// 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

PropTypeDefaultDescription
popupsarray—Enabled popups from the server loader. Omit it to load GET /v1/popups in the browser.
classNamesobject—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.
themeobject—Token overrides for this widget only, merged over the provider theme (same shape as PandaProvider.theme).
renderfunction—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 loader

The tenant's enabled popups with their targeting (GET /v1/popups); [] when none. Returns WidgetPopup[] → popups

fetchPopups()typed data

The 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.