Sign in
Panda visitor sign-in rendered inline (not a dialog): the same panels, API calls and session as the provider's auth dialog (email + password, Continue with Google, sign-up with phone + consent, forgot / reset password),…
Anatomy
Every piece of Sign in 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<PandaSignIn />complete widgetPanda visitor sign-in rendered inline (not a dialog): the same panels, API calls and session as the provider's auth dialog (email +…
<PandaAuthRoot>Holds the data and state its parts and hooks read<AuthStep />The step on screen with its tabs and Back link: the whole Panda flow in one part.
<AuthWhen />Its children only on mode (a step or a list): your own heading, image or copy per step.
<AuthLogin />Login form (email, password, Google, Forgot password → forgot step), always shown.
<AuthSignup />Sign-up form with the required marketing consent (Google sign-up asks phone + consent first).
<AuthForgotPassword />Email for the reset (link + 6-digit code), then the code step.
<AuthVerifyCode />The 6-digit code of the email (15 minutes, 5 attempts), then the new password step.
<AuthResetPassword />New password + confirm; signs the visitor in.
<AuthTabs />Login / Sign Up switch in the design's tab style (on those steps).
<AuthGoogle />Continue with Google on its own, anywhere in your layout (pair with design={{ providers: "none" }} so the login form…
<AuthDivider />The "OR" line (design dividerLabel, align), anywhere in your layout.
<AuthBackLink />Back to Login on the forgot and code steps.
<AuthLegal />Privacy Policy and Terms of Service links of the site.
</PandaAuthRoot><PandaAuthButton />complete widgetHeader account entry: signed out it opens the auth dialog (styled by the theme); signed in the avatar opens the account menu (collections,…
<PandaAuthCallback />complete widgetGoogle sign-in return page: render it at /auth/callback; it exchanges ?ott= for the visitor session and redirects.
</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/login/page.tsximport { PandaSignIn } from "@pandaidx/widgets/client"export default function LoginPage() {return (<main className="flex min-h-[70vh] items-center justify-center px-6 py-24"><PandaSignIn title="Agent Login" modes={["login"]} redirectTo="/" /></main>)}// Editorial login section: underline fields with floating labels, Google on top, text-link switch, arrow submit<PandaSignIntitle="Welcome back"align="start"tabsStyle="text-link"providers="top"providerStyle="outline"dividerLabel="or with email"fieldVariant="underline"labelStyle="floating"size="lg"buttonVariant="link-arrow"forgotPlacement="below-password"legal="compact"labels={{ signupTitle: "Create your account", loginButton: "Sign in", toSignupLink: "Join now" }}classNames={{ root: "max-w-lg border-0 bg-transparent p-0" }}/>// components/login-dialog.tsx ("use client"): the site's own shadcn Dialog around the widget (no Panda trigger)// <Dialog><DialogTrigger asChild><Button>Sign in</Button></DialogTrigger>// <DialogContent className="sm:max-w-md">// <DialogTitle className="sr-only">Sign in</DialogTitle>// <PandaSignIn title="" size="sm" tabsStyle="underline" classNames={{ root: "border-0 p-0" }} />// </DialogContent>// </Dialog>
Examples
Custom login built from the headless hook
"use client"import { usePandaAuthForm } from "@pandaidx/widgets/client"import { Button } from "@/components/ui/button"import { Input } from "@/components/ui/input"export const LoginCard = () => {const auth = usePandaAuthForm({ redirectTo: "/account" })if (auth.mode !== "login") return null // or render the other steps from auth.fieldsreturn (<form onSubmit={auth.submit} className="flex flex-col gap-4"><h2>Welcome back</h2><Button type="button" variant="outline" onClick={auth.loginWithGoogle}>Continue with Google</Button><p className="text-center text-sm">or</p><Input type="email" placeholder="Email" {...auth.field("email")} />{auth.errors.email ? <p role="alert">{auth.errors.email}</p> : null}<Input type="password" placeholder="Password" {...auth.field("password")} />{auth.errors.form ? <p role="alert">{auth.errors.form}</p> : null}<Button type="submit" disabled={auth.pending}>Sign in</Button><Button type="button" variant="ghost" onClick={auth.forgotPassword}>Forgot password?</Button></form>)}
Rearranged sign-in (Google first, no tabs, compact legal)
<PandaSignIn title="Welcome back" modes={["login"]} parts={{ tabs: false, google: "top", divider: "or with email", forgot: "below-password", legal: "compact" }} />
Header account link of your own
"use client"import { usePandaAuth } from "@pandaidx/widgets/client"export const AccountLink = () => {const { signedIn, user, openAuth, openDrawer } = usePandaAuth()return signedIn ? (<button type="button" onClick={() => openDrawer("collections")}>{user?.name ?? "My account"}</button>) : (<button type="button" onClick={() => openAuth("login")}>Sign in</button>)}
Sign-in as a page instead of the dialog
// app/layout.tsx: <PandaProvider … open={{ auth: { mode: "page", href: "/sign-in" } }}>// app/sign-in/page.tsxexport default async function SignIn({ searchParams }: { searchParams: Promise<{ mode?: string; redirectTo?: string }> }) {const { mode, redirectTo } = await searchParamsreturn <PandaSignIn defaultMode={mode === "signup" ? "signup" : "login"} signedIn="redirect" redirectTo={redirectTo ?? "/"} />}
Login and sign-up side by side
<PandaAuthRoot redirectTo="/account" design={{ providers: "bottom", fieldVariant: "underline" }}><div className="grid gap-12 md:grid-cols-2"><section><h2>Welcome back</h2><AuthWhen mode={["login", "signup"]}><AuthLogin /></AuthWhen><AuthWhen mode={["forgot-password", "verify-token", "reset-password"]}><AuthStep /></AuthWhen></section><section><h2>New here?</h2><AuthSignup /><AuthLegal /></section></div></PandaAuthRoot>
Stepper with your own heading per step
<PandaAuthRoot defaultMode="signup"><AuthWhen mode="signup"><h1>Create your account</h1></AuthWhen><AuthWhen mode="login"><h1>Sign in</h1></AuthWhen><AuthWhen mode={["forgot-password", "verify-token", "reset-password"]}><h1>Reset your password</h1></AuthWhen><AuthStep /><AuthLegal /></PandaAuthRoot>
Google above the tabs
<PandaAuthRoot design={{ providers: "none", dividerLabel: "or" }}><AuthWhen mode={["login", "signup"]}><AuthGoogle /><AuthDivider /></AuthWhen><AuthStep /><AuthLegal /></PandaAuthRoot>
API reference
PandaSignIn
Panda visitor sign-in rendered inline (not a dialog): the same panels, API calls and session as the provider's auth dialog (email + password, Continue with Google, sign-up with phone + consent, forgot / reset password), styled by the theme (dialog, input and button tokens). Use it for a /login page or an "Agent Login" / login box; login on a Panda site is ALWAYS this widget or PandaAuthButton, never a hand-built password form or a link/form to the Panda dashboard. Signed in it shows a small account summary (My Account, Sign Out), nothing, or redirects. It has no trigger button: it is the block you place in a page, a section, or inside the site's own Dialog / Sheet (<DialogContent><PandaSignIn …/></DialogContent>). Rearrange and restyle it with params, never CSS on its markup: switch (tabs, tabsStyle, tabsAlign), Google (providers, providerStyle, divider, dividerLabel), fields (fieldVariant, labelStyle, size), submit (buttonVariant, buttonWidth, buttonAlign), forgotPlacement, align, legal, copy (labels) and classNames slots. Every flow (verify → reset, forgot, Google, sign-up consent) works in every arrangement.
Import from @pandaidx/widgets/client · renders inside PandaProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultMode | "login" | "signup" | "login" | Panel shown first (falls back to the first of modes). |
| modes | "login" | "signup" | ["login","signup"] | Entry modes on offer; ["login"] hides Sign Up (an agents-only login box). |
| title | string | "Sign In" | Heading above the panels (every mode unless labels sets a title per mode); an empty string hides it. |
| description | string | — | Short copy under the heading. |
| redirectTo | string | — | Page to open after signing in or signing up. Default: stay on the page and show the signed-in state. |
| signedIn | "account" | "hidden" | "redirect" | "account" | Already signed in: a small account summary (My Account, Sign Out), nothing, or go to redirectTo (else /). |
| tone | "light" | "dark" | "light" | light: the theme surface; dark: a dark card (theme ink) with light text for dark sections and dark dialogs. |
| tabs | boolean | true | Show the Login / Sign Up switch (only with two modes). false = only defaultMode is reachable. |
| tabsStyle | "segmented" | "underline" | "text-link" | "segmented" | Switch look: segmented pill tabs, underline tabs with a bottom line, text-link no tabs but a "Don't have an account? Sign up" / "Already have an account? Log in" line under the form (placed by align). |
| tabsAlign | "stretch" | "start" | "center" | "end" | "stretch" | stretch tabs share the card width; start / center / end size them to their labels and place them. |
| providers | "top" | "bottom" | "none" | "bottom" | Continue with Google in the login panel: top above the email fields, bottom under the form, none email only (sign-up becomes the single email form). In sign-up the Google choice always comes after the phone + consent (Google provides neither). |
| providerStyle | "outline" | "solid" | "ghost" | "outline" | Google / "Create account with email" buttons: outline field-shaped box, solid filled with the theme button colors, ghost text button. |
| divider | boolean | true | The line between the email form and Google. |
| dividerLabel | string | "OR" | Divider text; an empty string draws a plain line. |
| fieldVariant | "outline" | "filled" | "underline" | — | Field look (PandaForm vocabulary): outline bordered box, filled tinted box without border, underline bottom line only (square, no side padding). Omitted = the theme's input design (DESIGN.md), else the native box. Retune it with --panda-auth-field-bg, --panda-auth-field-line, --panda-auth-field-focus, --panda-auth-field-radius, --panda-auth-field-height on classNames.root. |
| labelStyle | "stacked" | "floating" | "hidden" | "hidden" | hidden placeholders only (labels still announced to screen readers), stacked labels above the fields, floating small labels inside the field box. |
| size | "sm" | "md" | "lg" | "md" | Field, provider and submit height and type: sm 40px (compact dialogs), md 44px, lg 56px fields (hero login). |
| buttonVariant | "solid" | "outline" | "ghost" | "link-arrow" | "solid" | Submit look (PandaForm vocabulary): solid the theme button, outline, ghost text button, link-arrow text link with an arrow (always auto width). Radius, case and tracking come from the theme button tokens. |
| buttonWidth | "full" | "auto" | "full" | full stretches the submit across the card; auto sizes it to its label (placed by buttonAlign). |
| buttonAlign | "start" | "center" | "end" | — | Placement of an auto / link-arrow submit. Default: align. |
| forgotPlacement | "below-password" | "below-button" | "hidden" | "below-button" | "Forgot password?": below-password small link at the end under the password, below-button text button under the submit, hidden none (the reset flow is then unreachable from this box). |
| align | "start" | "center" | "center" | Alignment of the heading, the text-link switch, the divider label, the legal links and an auto submit. Fields always fill the width. |
| legal | "show" | "compact" | "hidden" | "show" | Privacy / Terms links row under the panels: show, compact (smaller, tighter line) or hidden. The sign-up consent checkbox (with its Terms / Privacy links) always stays: it is required. |
| labels | object | — | Copy per instance (wins over the dictionary). Titles per mode replace title in the heading. |
| classNames | object | — | Host classes per slot; plain Tailwind wins over the widget styles at every breakpoint (no !). Field colors, borders, radius and height set by fieldVariant / size / labelStyle follow the --panda-auth-field-* variables on root (e.g. [--panda-auth-field-bg:#f5f1ea]). |
| parts | object | — | Which parts render and where, in one object (wins over the flat props it covers). Each part reads the design kit by default: the title the dialog.title* tokens, the card dialog.padding / paddingMobile, the tabs the auth.tabs part (pill track), fields input.*, Google the field height and corners, the submit button.*. Example: { header: false, tabs: "underline", google: "top", divider: "or", forgot: "below-password", legal: "compact" }. |
PandaAuthRoot
Root of a sign-in flow you lay out yourself: one part per step (login, sign-up, forgot password, 6-digit code, new password), the Panda handlers (session, Google, consent, reset by link or code) and the PandaSignIn panel design, no container. A two-column login + sign-up page, a stepper with your own heading per step, or your own surface around the Panda forms.
Import from @pandaidx/widgets/client · renders inside PandaProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| defaultMode | "login" | "signup" | "forgot-password" | "verify-token" | "reset-password" | "login" | Step on load. |
| modes | "login" | "signup" | ["login","signup"] | Entry modes of AuthTabs / AuthStep. |
| redirectTo | string | — | Site path (/path) after login / sign-up / reset; default stays on the page. |
| design | object | — | The PandaSignIn design fields: tabs, tabsStyle, providers, providerStyle, divider, fieldVariant, labelStyle, size, buttonVariant, buttonWidth, buttonAlign, forgotPlacement, align, labels. |
| classNames | object | — | Classes per panel part: root, tabs, tab, field, input, label, button, provider, divider, footer. |
| className | string | — | Class of the root element. |
| popup | object | — | The popup this flow opened in ({ id, snapshot }): sign-ups are attributed to it. PopupAuth sets it. |
| children* | node | — | Your layout of the auth parts. |
PandaAuthButton
Header account entry: signed out it opens the auth dialog (styled by the theme); signed in the avatar opens the account menu (collections, saved searches, likes, My Account, sign out). A header entry in your own markup (your icon, text, menu) is usePandaAuth() ({ user, signedIn, openAuth(), openDrawer(tab?), logout() }): never rebuild the dialog or the drawer.
Import from @pandaidx/widgets/client · renders inside PandaProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "text" | "icon" | "avatar" | "text" | Trigger: "Sign In / Sign Up" text (name beside the avatar once signed in), a user icon, or an avatar circle. |
| label | string | "Sign In / Sign Up" | Signed-out text (text) or accessible name (icon, avatar). |
| className | string | — | Classes of the trigger (host typography/colors). |
| avatarClassName | string | — | Classes of the avatar. |
| onAction | function | — | Runs after the visitor picks an entry (e.g. close a mobile menu). |
| items | "collections" | "saved" | "likes" | "account" | "dashboard" | "logout" | ["collections","saved","likes","account","logout"] | Account menu entries in order (dashboard shows to the site owner only). For an inline menu or your own entries use PandaAccountMenu. |
| icons | boolean | false | Built-in icon before each account menu entry. |
PandaAuthCallback
Google sign-in return page: render it at /auth/callback; it exchanges ?ott= for the visitor session and redirects.
Import from @pandaidx/widgets/client · renders inside PandaProvider
| Prop | Type | Default | Description |
|---|---|---|---|
| redirectTo | string | "/" | Where to land after signing in. |
Parts
<AuthStep />The step on screen with its tabs and Back link: the whole Panda flow in one part.
<AuthWhen />Its children only on mode (a step or a list): your own heading, image or copy per step.
<AuthLogin />Login form (email, password, Google, Forgot password → forgot step), always shown.
<AuthSignup />Sign-up form with the required marketing consent (Google sign-up asks phone + consent first).
<AuthForgotPassword />Email for the reset (link + 6-digit code), then the code step.
<AuthVerifyCode />The 6-digit code of the email (15 minutes, 5 attempts), then the new password step.
<AuthResetPassword />New password + confirm; signs the visitor in.
<AuthTabs />Login / Sign Up switch in the design's tab style (on those steps).
<AuthGoogle />Continue with Google on its own, anywhere in your layout (pair with design={{ providers: "none" }} so the login form shows no second one).
<AuthDivider />The "OR" line (design dividerLabel, align), anywhere in your layout.
<AuthBackLink />Back to Login on the forgot and code steps.
<AuthLegal />Privacy Policy and Terms of Service links of the site.
Hooks
usePandaAuth()inside PandaAuthRoot{ user, signedIn, openAuth("login" | "signup"), openDrawer(tab?), logout() } — import from @pandaidx/widgets/client; your own sign-in links, gates and account menus (sign-in opens the site's way: PandaProvider open.auth)
usePandaAuthFlow()inside PandaAuthRootusePandaAuthFlow({ redirectTo? }) → { login({ email, password }), signup({ name, email, phone, password, marketingConsent: true }), loginWithGoogle(), signupWithGoogle({ phone, marketingConsent }), requestReset(email) (email with link + 6-digit code), verifyCode(email, code) → { ok, token }, resetPassword(token, password, confirmPassword), pending } — each resolves { ok, message, errors }; your own login / sign-up / reset forms over the Panda session. A sign-up of your own shows a consent checkbox (publish checks it) and the Privacy / Terms links
usePandaAuthForm()inside PandaAuthRootusePandaAuthForm({ defaultMode?, modes?, redirectTo?, initialValues?, initialToken? }) → { mode, setMode(mode), modes, fields (of the step), values, setValue(field, value), field(name) → props for your <input> (name, value, onChange, autoComplete, aria-invalid), errors ({ field: message, form: server message }), consent { checked, setChecked, copy, companyName, privacyLink, termsLink, error }, submit(event?) (runs the step: login, sign-up, send code → verify-token, verify → reset-password, new password), loginWithGoogle(), signupWithGoogle() (checks phone + consent first), forgotPassword(), backToLogin(), pending } — a sign-in fully of your own markup (any order: Google first, no tabs, your title), same session and rules as the dialog. Sign-up must render consent.copy beside a checkbox bound to consent.checked, with Terms / Privacy linked