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 MarkdownMulti-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 & 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> Create your account to get started
Enter the 4-digit code sent to your email
Min 8 characters, one uppercase letter and one number
Add a passkey for password-free sign-ins
Stay in the loop
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> This account exists but the email address was never confirmed. Resend the confirmation email to finish setting it up.
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.
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> Event log
- Waiting for registration events… (use the wizard above)