Registration

A complete multi-step sign-up built from <u-registration-root>: you declare the step order once, and steps that don't apply — password for social sign-ups, email verification after a resume, legacy-account matching when disabled — skip themselves. Interrupted and invited users are handled too.

View this guide as Markdown

Multi-step registration wizard

The steps attribute defines the order: email, verification, internal matching, profile, password, passkey and newsletter opt-ins. u-registration-resume appears automatically when the email already has an unfinished registration, verification codes auto-send and auto-submit, and the passkey step is optional. Try it with a fresh email address.

  • Declarative steps — steps='[…]' — the SDK handles transitions and skipping
  • Email verification — auto-send 4-digit code with auto-submit
  • Resume support — unfinished registrations continue via email link
  • Legacy account matching — u-registration-internal-matching links existing records
  • Passkey enrollment — optional u-registration-passkey step
  • Newsletter opt-ins — collected during sign-up with topic preferences
<!-- steps defines the order; steps whose requirements don't apply are skipped
     automatically (e.g. password for social sign-ups, internal-matching when
     the feature is disabled in the tenant). -->
<u-registration-root
  id="registration-root"
  steps='["email","verification","internal-matching","profile","password","passkey","newsletters"]'>
  <u-error-message for="registration" class-name="mb-3 text-sm text-danger"></u-error-message>

  <!-- 1: email -->
  <u-registration-step name="email">
    <div class="flex flex-col gap-3">
      <p class="text-center text-sm text-text-light">Create your account to get started</p>
      <u-raw-field
        field="email"
        type="email"
        required
        placeholder="Enter your email"
        class-name="px-4 py-2 border border-border rounded-lg w-full"></u-raw-field>
      <u-error-message for="email" class-name="text-sm text-danger"></u-error-message>

      <!-- Appears automatically when a registration already exists for this email -->
      <u-registration-resume
        class-name="px-4 py-2 border border-primary-light text-primary bg-primary-soft rounded-lg text-center w-full cursor-pointer disabled:opacity-50">
        Resume my registration
        <span slot="success" class="block text-sm text-success-text"
          >Resume link sent — check your email.</span
        >
        <span slot="resend">Resend resume link</span>
      </u-registration-resume>

      <u-submit-button
        class-name="px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg disabled:opacity-50 w-full cursor-pointer">
        Continue
      </u-submit-button>
    </div>
  </u-registration-step>

  <!-- 2: email verification (skipped if already verified, e.g. after resume) -->
  <u-registration-step name="verification" requires-email-verification>
    <div class="flex flex-col gap-3">
      <p class="text-center text-sm text-text-light">Enter the 4-digit code sent to your email</p>
      <u-registration-email-verification
        auto-send
        class-name="flex justify-center gap-2"
        input-class-name="w-12 h-12 text-center text-xl font-bold border-2 border-border rounded-lg focus:border-primary focus:outline-none"
      ></u-registration-email-verification>
      <u-error-message for="verificationCode" class-name="text-center text-sm text-danger"
      ></u-error-message>
      <u-registration-resend
        class-name="px-4 py-2 border border-border rounded-lg text-center w-full cursor-pointer hover:bg-background-light disabled:opacity-50">
        Resend code
      </u-registration-resend>
    </div>
  </u-registration-step>

  <!-- 3: link a legacy account (skipped unless enabled in the tenant) -->
  <u-registration-step name="internal-matching">
    <u-registration-internal-matching
      class-name="flex flex-col gap-3"
      input-class-name="px-4 py-2 border border-border rounded-lg w-full"
      primary-button-class-name="px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg cursor-pointer"
      secondary-button-class-name="px-4 py-2 border border-border rounded-lg cursor-pointer hover:bg-background-light"
      error-class-name="text-sm text-danger"></u-registration-internal-matching>
  </u-registration-step>

  <!-- 4: profile -->
  <u-registration-step name="profile">
    <div class="flex flex-col gap-3">
      <div class="grid grid-cols-2 gap-3">
        <u-raw-field
          field="first_name"
          type="text"
          required
          placeholder="First name"
          class-name="px-4 py-2 border border-border rounded-lg w-full"></u-raw-field>
        <u-raw-field
          field="last_name"
          type="text"
          placeholder="Last name"
          class-name="px-4 py-2 border border-border rounded-lg w-full"></u-raw-field>
      </div>
      <u-error-message for="first_name" class-name="text-sm text-danger"></u-error-message>
      <div class="flex gap-2">
        <u-back-button
          class-name="px-4 py-2 border border-border rounded-lg cursor-pointer hover:bg-background-light"
          >Back</u-back-button
        >
        <u-submit-button
          class-name="flex-1 px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg disabled:opacity-50 cursor-pointer"
          >Continue</u-submit-button
        >
      </div>
    </div>
  </u-registration-step>

  <!-- 5: password (skipped for social/passwordless sign-ups) -->
  <u-registration-step name="password" requires-password>
    <div class="flex flex-col gap-3">
      <u-raw-field
        field="password"
        type="password"
        required
        placeholder="Create a password"
        pattern="^(?=.*[A-Z])(?=.*[0-9]).{8,}$"
        pattern-error-message="Min 8 characters, one uppercase letter and one number"
        class-name="px-4 py-2 border border-border rounded-lg w-full"></u-raw-field>
      <p class="text-xs text-text-muted">Min 8 characters, one uppercase letter and one number</p>
      <u-error-message for="password" class-name="text-sm text-danger"></u-error-message>
      <div class="flex gap-2">
        <u-back-button
          class-name="px-4 py-2 border border-border rounded-lg cursor-pointer hover:bg-background-light"
          >Back</u-back-button
        >
        <u-submit-button
          class-name="flex-1 px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg disabled:opacity-50 cursor-pointer"
          >Continue</u-submit-button
        >
      </div>
    </div>
  </u-registration-step>

  <!-- 6: passkey enrollment (optional — Continue skips it) -->
  <u-registration-step name="passkey">
    <div class="flex flex-col gap-3">
      <p class="text-center text-sm text-text-light">Add a passkey for password-free sign-ins</p>
      <u-registration-passkey
        class-name="px-4 py-2 text-white bg-dark hover:bg-dark-lighter rounded-lg disabled:opacity-50 w-full cursor-pointer">
        Add passkey
      </u-registration-passkey>
      <div class="flex gap-2">
        <u-back-button
          class-name="px-4 py-2 border border-border rounded-lg cursor-pointer hover:bg-background-light"
          >Back</u-back-button
        >
        <u-submit-button
          class-name="flex-1 px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg disabled:opacity-50 cursor-pointer"
          >Continue</u-submit-button
        >
      </div>
    </div>
  </u-registration-step>

  <!-- 7: newsletter opt-ins collected during sign-up -->
  <u-registration-step name="newsletters">
    <div class="flex flex-col gap-3">
      <p class="text-sm font-medium text-text-light">Stay in the loop</p>
      <label class="flex cursor-pointer items-center gap-3 text-sm">
        <u-registration-newsletter name="main" checked></u-registration-newsletter>
        Main Newsletter
      </label>
      <div class="ml-6 flex flex-col gap-2">
        <label class="flex cursor-pointer items-center gap-3 text-sm">
          <u-registration-newsletter-preference name="main" preference="news-updates"
          ></u-registration-newsletter-preference>
          News Updates
        </label>
        <label class="flex cursor-pointer items-center gap-3 text-sm">
          <u-registration-newsletter-preference name="main" preference="tips-tricks"
          ></u-registration-newsletter-preference>
          Tips &amp; Tricks
        </label>
      </div>
      <label class="flex cursor-pointer items-center gap-3 text-sm">
        <u-registration-newsletter name="weather"></u-registration-newsletter>
        Weather Forecast
      </label>
      <div class="flex gap-2">
        <u-back-button
          class-name="px-4 py-2 border border-border rounded-lg cursor-pointer hover:bg-background-light"
          >Back</u-back-button
        >
        <u-submit-button
          class-name="flex-1 px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg disabled:opacity-50 cursor-pointer"
          >Complete registration</u-submit-button
        >
      </div>
    </div>
  </u-registration-step>
</u-registration-root>

<u-signed-in>
  <p class="mt-3 text-sm text-success-text">
    You are signed in — registration is for new accounts.
  </p>
</u-signed-in>
Live demo

Create your account to get started

Resume my registration Resume link sent — check your email. Resend resume link Continue

Enter the 4-digit code sent to your email

Resend code
Back Continue

Min 8 characters, one uppercase letter and one number

Back Continue

Add a passkey for password-free sign-ins

Add passkey
Back Continue

Stay in the loop

Back Complete registration

You are signed in — registration is for new accounts.

Unconfirmed & invited accounts

Two account states commonly break naive login forms: users who registered but never confirmed their email, and users invited by an admin or import who never accepted. The sign-in flow routes each to its own step, where u-resend-confirmation-email and u-resend-invitation-email offer the fix — both rate-limit themselves after sending.

  • unconfirmed step — resend the confirmation email
  • invited step — resend the invitation email (expired-link recovery)
  • Built-in rate limiting — buttons disable with a countdown after sending
<!-- The sign-in flow routes special account states to dedicated steps:
     "unconfirmed" — registered but never confirmed the email
     "invited"     — invited by an admin/import but hasn't accepted yet -->
<u-signin-root class="flex flex-col gap-2">
  <u-signin-step name="email">
    <u-email-field
      placeholder="Enter your email"
      class-name="px-4 py-2 mb-2 border border-border rounded-lg w-full"></u-email-field>
    <u-error-message for="email" class-name="mb-2 text-sm text-danger"></u-error-message>
    <u-submit-button
      for="email"
      text="Continue"
      class-name="px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg disabled:opacity-50 w-full cursor-pointer"
    ></u-submit-button>
  </u-signin-step>

  <u-signin-step name="unconfirmed">
    <u-back-button class-name="mb-3 text-sm text-primary cursor-pointer">← Back</u-back-button>
    <div class="rounded-lg bg-warning-bg p-4 text-sm text-warning-text">
      <p class="mb-3">
        This account exists but the email address was never confirmed. Resend the confirmation email
        to finish setting it up.
      </p>
      <u-resend-confirmation-email
        class-name="px-4 py-2 rounded-lg border border-warning-border text-sm font-medium cursor-pointer hover:bg-warning-border/40 disabled:opacity-50">
        Resend confirmation email
      </u-resend-confirmation-email>
    </div>
  </u-signin-step>

  <u-signin-step name="invited">
    <u-back-button class-name="mb-3 text-sm text-primary cursor-pointer">← Back</u-back-button>
    <div class="rounded-lg bg-primary-soft p-4 text-sm">
      <p class="mb-3 text-text">
        This account was created by invitation and hasn't been accepted yet. Resend the invitation
        email so the user can set their password — the button rate-limits itself with a countdown
        after each send.
      </p>
      <u-resend-invitation-email
        class-name="px-4 py-2 rounded-lg bg-accent text-white text-sm font-medium cursor-pointer hover:bg-accent-hover disabled:opacity-50">
        Resend invitation email
      </u-resend-invitation-email>
    </div>
  </u-signin-step>
</u-signin-root>
Live demo
← Back

This account exists but the email address was never confirmed. Resend the confirmation email to finish setting it up.

Resend confirmation email
← Back

This account was created by invitation and hasn't been accepted yet. Resend the invitation email so the user can set their password — the button rate-limits itself with a countdown after each send.

Resend invitation email

Registration events

u-registration-root emits stepChange on every transition, registrationComplete when the account is created, and errorEvent on failures — wire them up with plain addEventListener for progress tracking, analytics, or post-signup redirects.

  • stepChange / registrationComplete / errorEvent — plain DOM events
  • Live event log — walk through the wizard and watch
<div class="rounded-lg border border-border bg-background-light p-4">
  <p class="mb-2 text-sm font-medium">Event log</p>
  <ul id="registration-event-log" class="flex flex-col gap-1 font-mono text-xs text-text-light">
    <li>Waiting for registration events… (use the wizard above)</li>
  </ul>
</div>

<script is:inline>
  (() => {
    const log = (message) => {
      const entry = document.createElement("li");
      entry.textContent = `${new Date().toLocaleTimeString()} ${message}`;
      document.getElementById("registration-event-log")?.prepend(entry);
    };

    const registration = document.getElementById("registration-root");

    // Fired on every step transition — drive progress UIs or analytics from it
    registration?.addEventListener("stepChange", (event) => {
      log(`stepChange → ${event.detail?.stepName ?? JSON.stringify(event.detail)}`);
    });

    // Fired once the account is created
    registration?.addEventListener("registrationComplete", (event) => {
      log(`registrationComplete (${JSON.stringify(event.detail ?? {})})`);
    });

    registration?.addEventListener("errorEvent", (event) => {
      log(`errorEvent — ${event.detail?.error ?? "unknown error"}`);
    });
  })();
</script>
Live demo

Event log

  • Waiting for registration events… (use the wizard above)