DHxWhy

Progressive Sign Up Form – Fields Appear as You Go, No Steps

Forms

About

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>
  );
}

Dependencies

More by DHxWhy

View all
  • Product Rail Gallery – Fashion Store Hero Section

    Product Rail Gallery – Fashion Store Hero Section

    2.2K
  • Sushi Conveyor Belt Menu – Restaurant Hero Section

    Sushi Conveyor Belt Menu – Restaurant Hero Section

    883
  • Team Lanyard Rail – Meet the Team Section with Flip Badges

    Team Lanyard Rail – Meet the Team Section with Flip Badges

    827
  • Skateboard Deck Rail Gallery – Skate Shop Hero Section

    Skateboard Deck Rail Gallery – Skate Shop Hero Section

    430
  • Sun Moon Dark Mode Theme Toggle

    Sun Moon Dark Mode Theme Toggle

    328
  • Picture Rail Image Gallery – Art Print Shop Hero Section

    Picture Rail Image Gallery – Art Print Shop Hero Section

    268
  • Pendant Lamp Rail Gallery – Lighting Store Hero Section

    Pendant Lamp Rail Gallery – Lighting Store Hero Section

    243
  • World Testimonials Globe – Social Proof Hero Section

    World Testimonials Globe – Social Proof Hero Section

    217

More in Forms

View all
  • Premium Auth

    Premium Auth

    543
  • Contact Card

    Contact Card

    1.1K
  • Form

    Form

    17
  • Card Studio

    Card Studio

    46
  • Book A Demo 1

    Book A Demo 1

    80
  • Login 2

    Login 2

    10
  • Login 1

    Login 1

    58
  • SignInPage

    SignInPage

    710