Progressive Sign Up Form for React and Tailwind CSS (shadcn/ui): one card that starts with an email field and grows the next field only when the previous one is valid, with no step pages. The name slides in under a valid email, the password brings a four-segment strength meter that fills rule by rule, and the terms row arrives as the button morphs from Continue to Create account. Earlier fields stay editable. An async onSubmit fills the button, then the card morphs into a welcome state or offers Try again. Use it for SaaS onboarding and newsletter sign-ups. The demo types a sign-up, fails once and retries, with no requests. Requires React only.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Progressive Sign Up Form – Fields Appear as You Go, No Steps to your project and fits it to your theme.
Code
How Progressive Sign Up Form – Fields Appear as You Go, No Steps is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client";
import * as React from "react";
import { ProgressiveSignUpForm } from "@/components/ui/progressive-sign-up-form";
import type { SignUpStatus, SignUpStep, SignUpValues } from "@/components/ui/progressive-sign-up-form";
const EMAIL = "jordan@alder.studio";
const NAME = "Jordan Lee";
const PASSWORD = "Alder!2026";
const EMPTY: SignUpValues = { email: "", name: "", password: "", agreed: false };
const ALL_STEPS: SignUpStep[] = ["email", "name", "password", "terms"];
export default function Demo({ autoplay = true }: { autoplay?: boolean }) {
const params = React.useMemo(() => new URLSearchParams(typeof location === "undefined" ? "" : location.search), []);
const speed = Number(params.get("speed") ?? 1) || 1;
const fail = params.get("fail");
const latency = Number(params.get("delay") ?? 1000) || 1000;
const uncontrolled = params.get("uncontrolled") === "1";
const prefill = params.get("prefill") === "1";
const noSocial = params.get("nosocial") === "1";
const revealDelay = params.get("reveal") === null ? (speed === 1 ? undefined : Math.round(300 * speed)) : Number(params.get("reveal")) || 0;
const steps = React.useMemo(() => {
const wanted = params.get("steps")?.split(",") ?? [];
const picked = ALL_STEPS.filter((step) => wanted.includes(step));
return picked.length > 0 ? picked : undefined;
}, [params]);
const [values, setValues] = React.useState(prefill ? { ...EMPTY, email: EMAIL, name: NAME } : EMPTY);
const [run, setRun] = React.useState(0);
const [paused, setPaused] = React.useState(false);
const [status, setStatus] = React.useState("idle" as SignUpStatus);
const [step, setStep] = React.useState("email" as SignUpStep | "done");
const [opened, setOpened] = React.useState(0);
const rootRef = React.useRef(null as HTMLDivElement | null);
const attempts = React.useRef(0);
const runRef = React.useRef(0);
runRef.current = run;
const failsFirst = (index: number) => fail === "1" || (fail !== "0" && index % 2 === 1);
React.useEffect(() => {
if (!autoplay || paused) return;
const timers: number[] = [];
const at = (ms: number, task: () => void) => timers.push(window.setTimeout(task, Math.round(ms * speed)));
const field = (name: SignUpStep) => rootRef.current?.querySelector(`[data-field="${name}"]`);
const focus = (name: SignUpStep) => { const node = field(name); if (node instanceof HTMLElement) node.focus({ preventScroll: true }); };
const type = (name: "email" | "name" | "password", content: string, start: number, perChar: number) => {
for (let index = 1; index <= content.length; index += 1) at(start + (index - 1) * perChar, () => setValues((current) => ({ ...current, [name]: content.slice(0, index) })));
};
const submit = () => rootRef.current?.querySelector("form")?.requestSubmit();
attempts.current = 0;
at(300, () => focus("email"));
type("email", EMAIL, 400, 74);
at(2300, () => focus("name"));
type("name", NAME, 2300, 80);
at(3600, () => focus("password"));
type("password", PASSWORD, 3600, 80);
at(5300, () => { focus("terms"); setValues((current) => ({ ...current, agreed: true })); });
at(5800, submit);
const retry = failsFirst(run);
if (retry) at(8000, submit);
at(retry ? 13000 : 11000, () => { setValues(EMPTY); setRun((previous) => previous + 1); });
return () => timers.forEach((timer) => window.clearTimeout(timer));
}, [autoplay, paused, run, speed, fail]);
const takeOver = (event: React.SyntheticEvent) => {
if (!event.isTrusted || !autoplay) return;
setPaused(true);
};
const create = async () => {
const index = runRef.current;
attempts.current += 1;
await new Promise((resolve) => window.setTimeout(resolve, Math.round(latency * speed)));
if (failsFirst(index) && attempts.current === 1) throw new Error("We couldn't reach the server. Check your connection and try again.");
};
return (
<main
data-demo-root
data-run={run}
data-host-status={status}
data-host-step={step}
data-opened={opened}
onPointerDownCapture={takeOver}
onKeyDownCapture={takeOver}
className="flex min-h-[min(100svh,760px)] w-full items-center justify-center bg-background px-6 py-6 text-foreground [@media(max-height:680px)]:py-3"
>
<div ref={rootRef} className="flex w-full justify-center">
<ProgressiveSignUpForm
key={run}
brandName="Alder"
steps={steps}
signInHref="#"
termsHref="#"
privacyHref="#"
social={noSocial ? undefined : [{ label: "Continue with Google" }, { label: "Continue with GitHub" }]}
revealDelay={revealDelay}
onSubmit={create}
onStatusChange={setStatus}
onStepChange={setStep}
onComplete={() => setOpened((count) => count + 1)}
{...(uncontrolled ? { defaultValues: prefill ? { email: EMAIL, name: NAME } : undefined } : { values, onValuesChange: setValues })}
/>
</div>
</main>
);
}
















