Forms

A Panda dynamic form (contact, home valuation, request showing, new development, or any form by id) with the websites field renderers.

Fully headlessClient component@pandaidx/widgets 1.14.1

Anatomy

Every piece of Forms 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
<PandaForm />complete widget

A Panda dynamic form (contact, home valuation, request showing, new development, or any form by id) with the websites field renderers.

<PandaFormRoot>Holds the data and state its parts and hooks read
<FormFields />

The fields of the step on screen in the form builder rows (every field of a one-step form), minus the ones a FormField…

<FormField />

One field by id or CRM mapping (name, email, phone, message), anywhere: your own grid.

<FormStep />

Its children only while step (1-based) is on screen: your own layout per step.

<FormProgress />

"Step 2 of 3", the step title and description from the form builder (when set) + progress bar (nothing on one-step…

<FormSubmit />

Next on intermediate steps (checks the step), the form's submit text on the last one (nextLabel, label); shows the send…

<FormBack />

Back to the previous step (label).

<FormActions />

The action row of the Panda form: Back (from step 2), Next / submit, the send error and the legal links (nextLabel,…

<FormLegal />

Privacy | Terms links when the form builder enables them.

<FormSuccess />

Shown after a successful submit: the message and the Download link (afterSubmit download), or your own children.

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

PandaForm
// app/contact/page.tsx: the contact form on a dark section, matched to the site design
import { PandaForm } from "@pandaidx/widgets/client"
import { fetchForm } from "@pandaidx/widgets/server"
export default async function ContactPage() {
const form = await fetchForm({ publicKey: process.env.NEXT_PUBLIC_PANDA_PUBLIC_KEY!, apiUrl: process.env.NEXT_PUBLIC_PANDA_API_URL, spot: "contact" })
return (
<section className="bg-neutral-950 py-24">
<div className="mx-auto max-w-3xl px-6">
<PandaForm
form={form}
tone="dark"
fieldVariant="underline"
labelStyle="hidden"
columns={2}
size="lg"
buttonVariant="outline"
buttonWidth="auto"
align="center"
submitLabel="Send message"
/>
</div>
</section>
)
}
// components/guide-dialog.tsx ("use client"): lead magnet in the site's shadcn Dialog; the PDF downloads after the submit
// <DialogContent className="sm:max-w-lg">
// <DialogHeader><DialogTitle>Miami Buyer's Guide</DialogTitle></DialogHeader>
// <PandaForm formId={formId} size="sm" labelStyle="floating" columns={2} submitLabel="Get the free guide"
// afterSubmit={{ type: "download", url: guideUrl, filename: "miami-buyers-guide.pdf" }}
// successMessage="Your guide is downloading." contextData={{ campaign: "buyers-guide" }} />
// </DialogContent>

Examples

Typeform-style screens (one step per screen, the step's own heading)

PandaForm
"use client"
import { FormActions, FormFields, FormSuccess, PandaFormRoot, usePandaForm } from "@pandaidx/widgets/client"
const StepHeading = () => {
const { step, stepCount, stepInfo, submitted } = usePandaForm()
if (submitted) return null
return (
<header className="mb-8">
<p className="text-sm opacity-60">{step + 1} / {stepCount}</p>
{stepInfo.title ? <h2 className="text-4xl">{stepInfo.title}</h2> : null}
{stepInfo.description ? <p className="mt-2 opacity-70">{stepInfo.description}</p> : null}
</header>
)
}
export const QuizForm = ({ formId }: { formId: string }) => (
<PandaFormRoot formId={formId} size="lg" fieldVariant="underline" buttonWidth="auto">
<StepHeading />
<FormFields />
<FormActions nextLabel="OK" />
<FormSuccess />
</PandaFormRoot>
)
// Step titles and descriptions come from the form (dashboard builder or create_form `steps`). The same form works inline, in a dialog or behind a floating video badge.

Two-step form with your own layout per step

PandaForm
<PandaFormRoot spot="contact" buttonWidth="auto">
<FormProgress />
<FormStep step={1}>
<div className="grid gap-4 md:grid-cols-2"><FormField name="name" /><FormField name="email" /></div>
</FormStep>
<FormStep step={2}>
<FormField name="phone" />
<FormField name="message" />
</FormStep>
<div className="flex items-center gap-4"><FormBack /><FormSubmit nextLabel="Continue" /></div>
<FormLegal />
<FormSuccess />
</PandaFormRoot>
// Steps come from the form (create_form `step` per field, or the dashboard builder's row Step); FormStep only places them.

Your own inputs with the headless hook

PandaForm
"use client"
import { usePandaForm } from "@pandaidx/widgets/client"
// Every Panda form has a required name and email: render both (your inputs or FormField).
export const QuickLead = () => {
const { fields, values, setValue, errors, submit, submitting, submitted, failure } = usePandaForm()
if (submitted) return null
const field = (mapping: string) => fields.find((entry) => entry.crmFieldMapping === mapping)
return (
<div className="flex flex-col gap-2">
{["name", "email"].map((mapping) => {
const input = field(mapping)
return input ? <input key={input.id} placeholder={input.label} value={String(values[input.id] ?? "")} onChange={(event) => setValue(input.id, event.target.value)} aria-invalid={Boolean(errors[input.id])} /> : null
})}
<button type="button" disabled={submitting} onClick={() => void submit()}>Send</button>
{failure ? <p role="alert">{failure}</p> : null}
</div>
)
}
// <PandaFormRoot spot="newsletter"><QuickLead /><FormSuccess /></PandaFormRoot>

API reference

PandaForm

A Panda dynamic form (contact, home valuation, request showing, new development, or any form by id) with the websites field renderers. Values are validated in the browser with the API rules, then posted to POST /v1/forms/submit (the lead lands in the tenant's CRM like a website lead). After submit: success message, a document download, or a redirect. Never submit it from automated tests: each submit creates a real contact.

Import from @pandaidx/widgets/client · renders inside PandaProvider

PropTypeDefaultDescription
formobject—Form definition from the server loader (null = nothing assigned → renders nothing). Omit it to load spot / formId in the browser.
spot"contact" | "home-valuation" | "request-showing" | "new-development" | "newsletter" | "careers"—Page spot whose assigned form is rendered (browser load when form is omitted).
formIdstring—A form of the tenant by id (wins over spot).
titlestring—Heading above the form.
submitLabelstring—Replaces the submit button text of the form builder.
layout"schema" | "stacked" | "two-column""schema"schema draws the form's own rows: which fields sit side by side is part of the form (the owner's form panel, arrange_form), so the page always matches it. stacked one field per row, for a narrow column. two-column is kept for old sites and draws the form's rows too. Never fake widths with CSS spans on fields.
columns1 | 2—Shorthand of layout: 1 one field per row (a narrow column); 2 is kept for old sites and draws the form's rows. Omitted = the form's rows (one column under 768px). To put fields side by side or full width, change the form with arrange_form.
fieldVariant"outline" | "filled" | "underline"—Field look: outline bordered box, filled tinted box (colors.soft) without border, underline bottom line only (editorial / luxury sections, square, no side padding). Omitted = the theme's input.variant (the site design system), else outline: set it per form only to deviate on purpose.
size"sm" | "md" | "lg""md"Density: control height 40/48/56px with matching padding, font size (14/16/17px) and default gap (12/16/22px). sm for compact dialogs and newsletter rows, lg for hero lead forms.
radiusstring—Field corner radius (CSS length: 0px, 6px, 9999px). Default: the theme's radius.control. Underline fields are always square; the button keeps button.radius.
labelStyle"stacked" | "floating" | "hidden"—stacked labels above the fields, floating small labels inside the field box, hidden no visible labels (the label becomes the placeholder, still announced to screen readers). Omitted = the form builder's "show labels" setting.
gapstring—Space between fields and rows (CSS length, e.g. 20px). Default by size.
buttonVariant"solid" | "outline" | "ghost" | "link-arrow"—Submit button look: solid primary fill, outline primary border that fills on hover, ghost text button, link-arrow text link with an arrow (always auto width). Omitted = the theme's button.variant. Radius, case and tracking always come from the theme button tokens.
buttonWidth"full" | "auto""full"full stretches the submit button across the form; auto sizes it to its label (placed by align).
align"start" | "center" | "end""start"Alignment of the title, the submit button (when buttonWidth is auto), the legal links and the success state. Fields always start-align.
tone"light" | "dark""light"Surface the form sits on. dark = light-on-dark fields, labels, consent and buttons (white solid button, white outline) for dark sections, dark cards and dark dialogs. Set it whenever the background behind the form is dark.
successMessagestring"Thank you! We will get back to you shortly."Shown instead of the form after a successful submit.
afterSubmitobject{"type":"message"}After a successful submit: { type: "download", url, filename? } starts the download of a document (e.g. a Media Center PDF) and keeps a Download link; { type: "redirect", url } goes to a page; { type: "message" } only shows the message. Only http(s) URLs and site paths are followed.
contextDataobject—Extra context stored with the lead (listing id, building, campaign…): { listingId: "A123" }.
sourcestring—Lead source label (defaults to the spot, else widget-form).
onSubmittedfunction—Runs after a successful submit (e.g. close the dialog the form is in).
classNamesobject—Host classes appended to slots: root, title, form, rows, row, field, label, input, consent, error, actions, button, legal, success. They win over the widget's Tailwind classes at every breakpoint (layout: max-w, margins, paddings, grid spans). Properties the form design stylesheet sets (field background, border, color, height, row gaps) follow the --panda-form-* variables on root (e.g. [--panda-form-field-bg:#f5f1ea]); only there Tailwind's important suffix (bg-white!) is the fallback.
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).

Parts

<FormFields />

The fields of the step on screen in the form builder rows (every field of a one-step form), minus the ones a FormField already places: <FormField name="email" /> in your grid + <FormFields /> for the rest.

<FormField />

One field by id or CRM mapping (name, email, phone, message), anywhere: your own grid.

<FormStep />

Its children only while step (1-based) is on screen: your own layout per step.

<FormProgress />

"Step 2 of 3", the step title and description from the form builder (when set) + progress bar (nothing on one-step forms).

<FormSubmit />

Next on intermediate steps (checks the step), the form's submit text on the last one (nextLabel, label); shows the send error.

<FormBack />

Back to the previous step (label).

<FormActions />

The action row of the Panda form: Back (from step 2), Next / submit, the send error and the legal links (nextLabel, label).

<FormLegal />

Privacy | Terms links when the form builder enables them.

<FormSuccess />

Shown after a successful submit: the message and the Download link (afterSubmit download), or your own children.

Hooks

usePandaForm()inside PandaFormRoot

{ form, fields, stepFields, values, setValue(id, value), errors, locked (fields filled from the signed-in visitor), step (0-based), stepCount, stepInfo ({ step, title?, description?, image? } the form gave the step on screen; image = the https URL the owner picked for it), isLastStep, next(), back(), submit(), submitting, submitted, failure, submitText, successMessage, download } — your own inputs with the same validation and the same lead submit

usePandaFormWarmup()inside PandaFormRoot

nothing: call usePandaFormWarmup({ spot }) (or { formId }) in the component that holds your own closed dialog with a PandaFormRoot; the form's code and schema load after the visitor's first interaction (never during the page load), so the dialog opens complete at its final height (no growth when the form arrives)

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.

fetchForm()widget loader

The form of a page spot (GET /v1/forms/:spot) or by id (GET /v1/forms/by-id/:id); null when none. Returns WidgetForm | null → form

fetchForm()typed data

The form definition (rows of fields with type, label, required, CRM mapping, options, step) for <PandaFormRoot form> or <PandaForm form>. Never post leads yourself: submit through the root (POST /v1/forms/submit with page, UTMs and contact attribution).