Authentication
Sign users in with email + password, magic codes, passkeys or social providers. <u-signin-root> drives a multi-step flow: you declare one <u-signin-step> per state, the SDK decides which one is active — and <u-conditional-render> adapts each step to what the account actually supports.
View this guide as MarkdownComplete sign-in flow
One u-signin-root covering the whole journey: email → verification (password, magic code, or reset), plus the magic-code, reset-password and missing-fields steps. Conditions like auth.passwordEnabled and auth.magicCodeEnabled make the same markup work for every account type — passwordless accounts are offered a password reset instead of a dead end.
- Multi-step state machine — one <u-signin-step> per state, SDK-controlled
- Adaptive verification — password / magic code / reset offered per account
- Missing fields — collects newly-required profile fields at sign-in
- Auto-hide when signed in — the form disappears once authenticated
<!-- The root manages the flow; each u-signin-step shows when its step is active.
The whole form hides automatically once the user is signed in. -->
<u-signin-root id="sign-in-root" class="flex flex-col gap-2">
<!-- Step 1: email -->
<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 with email"
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-error-message for="general" class-name="mt-2 text-sm text-danger"></u-error-message>
</u-signin-step>
<!-- Step 2: verification — offers exactly what the account supports -->
<u-signin-step name="verification">
<u-back-button class-name="mb-3 text-sm text-primary cursor-pointer">← Back</u-back-button>
<u-conditional-render when="auth.passwordEnabled">
<u-password-field
placeholder="Enter your password"
class-name="px-4 py-2 mb-1 border border-border rounded-lg w-full"></u-password-field>
<u-reset-password-button class-name="mb-3 text-sm text-primary cursor-pointer"
></u-reset-password-button>
<u-error-message for="password" class-name="mb-2 text-sm text-danger"></u-error-message>
<u-error-message for="resetPassword" class-name="mb-2 text-sm text-danger"></u-error-message>
<u-submit-button
for="password"
class-name="px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg w-full cursor-pointer"
></u-submit-button>
</u-conditional-render>
<u-conditional-render when="auth.magicCodeEnabled">
<u-conditional-render when="auth.magicCodeSent" is="false">
<u-send-magic-code-button
text="Send me a magic code instead"
class-name="mt-3 px-4 py-2 text-primary bg-primary-soft rounded-lg text-center w-full cursor-pointer disabled:opacity-50"
></u-send-magic-code-button>
</u-conditional-render>
</u-conditional-render>
<!-- Passwordless account? Offer to set one. -->
<u-conditional-render when="auth.passwordEnabled" not>
<u-reset-password-button class-name="mt-3 text-sm text-primary cursor-pointer">
Set a password for this account
</u-reset-password-button>
</u-conditional-render>
</u-signin-step>
<!-- Step 3 (magic code path) -->
<u-signin-step name="magic-code">
<p class="mb-3 text-center text-sm text-text-light">Enter the code we sent to your email</p>
<u-magic-code-field class-name="px-4 py-2 mb-2 rounded-lg"></u-magic-code-field>
<u-error-message for="magicCode" class-name="mb-2 text-center text-sm text-danger"
></u-error-message>
<u-send-magic-code-button
text="Resend magic code"
class-name="px-4 py-2 text-primary bg-primary-soft rounded-lg text-center w-full cursor-pointer disabled:opacity-50"
></u-send-magic-code-button>
</u-signin-step>
<!-- Reset-password path (opened via u-reset-password-button email link) -->
<u-signin-step name="reset-password">
<p class="mb-3 text-center text-sm text-text-light">Choose a new password</p>
<u-password-field
for="new-password"
placeholder="New password"
class-name="px-4 py-2 mb-2 border border-border rounded-lg w-full"></u-password-field>
<u-password-field
for="password-confirmation"
placeholder="Confirm password"
class-name="px-4 py-2 mb-2 border border-border rounded-lg w-full"></u-password-field>
<u-error-message for="resetPassword" class-name="mb-2 text-sm text-danger"></u-error-message>
<u-submit-button
for="resetPassword"
class-name="px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg w-full cursor-pointer"
></u-submit-button>
</u-signin-step>
<!-- Shown when the tenant requires profile fields the account is missing -->
<u-signin-step name="missing-fields">
<p class="mb-3 text-center text-sm text-text-light">One last step — complete your profile</p>
<u-missing-field></u-missing-field>
<u-missing-fields-submit-button></u-missing-fields-submit-button>
</u-signin-step>
<!-- Unknown email → the flow switches to this step -->
<u-signin-step name="registration">
<u-back-button class-name="mb-3 text-sm text-primary cursor-pointer">← Back</u-back-button>
<p class="text-sm text-text-light">
No account found for this email. See the
<a href="/registration" class="text-primary underline">registration flow demo</a>
for the full multi-step sign-up.
</p>
</u-signin-step>
</u-signin-root>
<u-signed-in>
<div class="rounded-lg bg-success-bg p-4 text-sm text-success-text">
You are signed in — the sign-in form hides itself automatically.
</div>
</u-signed-in> Enter the code we sent to your email
Choose a new password
One last step — complete your profile
No account found for this email. See the registration flow demo for the full multi-step sign-up.
Passkey sign-in
u-passkey performs WebAuthn discoverable-credential sign-in: the browser offers stored passkeys, no email entry required. Enrollment happens during registration (see the registration demo) or in the Unidy account UI.
- One-tap sign-in — Face ID, fingerprint, or device PIN
- Discoverable credentials — works without typing an email first
<!-- Discoverable passkey sign-in: no email needed, the browser offers stored passkeys -->
<u-signin-root class="flex flex-col gap-2">
<u-passkey
text="Sign in with passkey"
loading-text="Authenticating…"
class-name="px-4 py-2 text-white bg-dark hover:bg-dark-lighter rounded-lg disabled:opacity-50 w-full cursor-pointer transition-colors"
></u-passkey>
<u-error-message for="passkey" class-name="mt-2 text-sm text-danger"></u-error-message>
</u-signin-root>
<u-signed-in>
<p class="text-sm text-success-text">
Signed in — enroll passkeys during registration or from the Unidy account page.
</p>
</u-signed-in> Signed in — enroll passkeys during registration or from the Unidy account page.
Modal login
The same sign-in components work inside any dialog you own. A dozen lines of vanilla JS open/close the modal; listening to the authEvent closes it on success.
- Your modal, SDK inside — no special modal component needed
- authEvent-driven UX — close the dialog the moment sign-in succeeds
<!-- Only offer the modal to signed-out visitors — the sign-in root inside
would render empty for an authenticated user -->
<u-signed-in not>
<button id="open-login-modal" class="btn btn-primary">Sign in</button>
</u-signed-in>
<div id="login-modal" class="fixed inset-0 z-50 hidden items-center justify-center">
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" data-close-modal></div>
<div class="relative z-10 w-full max-w-md rounded-xl bg-white p-6 shadow-2xl">
<button
data-close-modal
aria-label="Close"
class="absolute right-4 top-3 text-2xl text-text-muted hover:text-text">×</button
>
<u-signin-root id="modal-signin" class="block w-full">
<u-signin-step name="email">
<h2 class="mb-1 text-center text-xl font-semibold">Welcome</h2>
<p class="mb-4 text-center text-sm text-text-light">Sign in or create an account</p>
<u-social-login-button provider="google" theme="dark"></u-social-login-button>
<div class="my-4 flex items-center">
<div class="flex-grow border-t border-border"></div>
<span class="mx-3 text-xs font-medium text-text-muted">OR</span>
<div class="flex-grow border-t border-border"></div>
</div>
<u-email-field 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="verification">
<u-back-button class-name="mb-3 text-sm text-primary cursor-pointer">← Back</u-back-button>
<u-password-field
placeholder="Enter your password"
class-name="px-4 py-2 mb-2 border border-border rounded-lg w-full"></u-password-field>
<u-error-message for="password" class-name="mb-2 text-sm text-danger"></u-error-message>
<u-submit-button
for="password"
class-name="px-4 py-2 text-white bg-accent hover:bg-accent-hover rounded-lg w-full cursor-pointer"
></u-submit-button>
</u-signin-step>
</u-signin-root>
</div>
</div>
<u-signed-in>
<p class="mt-3 text-sm text-success-text">Signed in — the modal closes on the authEvent.</p>
</u-signed-in>
<script is:inline>
(() => {
const modal = document.getElementById("login-modal");
const open = () => {
modal.classList.remove("hidden");
modal.classList.add("flex");
};
const close = () => {
modal.classList.add("hidden");
modal.classList.remove("flex");
};
document.getElementById("open-login-modal")?.addEventListener("click", open);
modal
.querySelectorAll("[data-close-modal]")
.forEach((el) => el.addEventListener("click", close));
document.addEventListener("keydown", (e) => e.key === "Escape" && close());
// Close the modal as soon as the user is authenticated
document.getElementById("modal-signin")?.addEventListener("authEvent", close);
})();
</script> Signed in — the modal closes on the authEvent.
Auth events
Every sign-in root emits DOM events you can wire up with plain addEventListener: authEvent on successful sign-in, errorEvent on failures, and logout from u-logout-button. This is how you sync your own UI (header state, redirects, analytics) with the SDK.
- authEvent / errorEvent / logout — plain DOM events, no SDK API needed
- Live event log — sign in above and watch the events arrive
<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="auth-event-log" class="flex flex-col gap-1 font-mono text-xs text-text-light">
<li>Waiting for auth events… (use the sign-in flow above)</li>
</ul>
</div>
<script is:inline>
(() => {
const log = (message) => {
const entry = document.createElement("li");
entry.textContent = `${new Date().toLocaleTimeString()} ${message}`;
document.getElementById("auth-event-log")?.prepend(entry);
};
// Events bubble from any u-signin-root on the page
document.querySelectorAll("u-signin-root").forEach((root) => {
root.addEventListener("authEvent", (event) => {
log(`authEvent — user signed in (${JSON.stringify(event.detail ?? {})})`);
});
root.addEventListener("errorEvent", (event) => {
log(`errorEvent — ${event.detail?.error ?? "unknown error"}`);
});
});
// Logout is emitted by u-logout-button
document.querySelectorAll("u-logout-button").forEach((button) => {
button.addEventListener("logout", () => log("logout"));
});
})();
</script> Event log
- Waiting for auth events… (use the sign-in flow above)
Social login #
u-social-login-button starts the provider's OAuth flow in a Unidy-hosted dialog. Providers are configured per tenant; custom providers and custom button icons are supported via the provider attribute and icon slot.
Signed in — social login buttons are hidden.