ElvixOnboarding
What happens after a redirect sign-in. Mount once; it opens itself on the step the server asked for (add a passkey, pick a username, restore a membership), finishes it in place and closes.
- No more password or email codes
- Phish-proof. Works only on this site.
- Syncs across your devices via iCloud, Google, or 1Password
"use client";import { ElvixOnboarding, ElvixProvider } from "@elvix.is/sdk/react";import { useRouter } from "next/navigation";export function Providers({ children }: { children: React.ReactNode }) {const router = useRouter();return (<ElvixProvider clientId={process.env.NEXT_PUBLIC_ELVIX_CLIENT_ID!}>{/* Mount once, high in the tree. Renders nothing until a redirectsign-in brings back a step, then opens itself on that step. */}<ElvixOnboardingsteps={["passkey"]}onResult={(r) => {if (r.ok) router.refresh();}}/>{children}</ElvixProvider>);}
Why it exists
Google, GitHub and the hosted passkey page send the person away and bring them back to your page. That is a full page load, so the in-frame sign-in is gone. When the server asked for one more step (pick a username, add a passkey, restore a membership), there is nothing on the page to show it. Email-code, username and passkey sign-ins stay on your page and show those steps in the same frame, which is why a person could be offered a passkey after a username sign-in and never after Google.
<ElvixOnboarding> closes that gap. It renders nothing until such a return arrives. Then it opens as a modal or drawer on the right pane for the step, lets the person finish it, calls onResult, and closes.
Props
| Prop | Type | Required | Default | Description |
|---|---|---|---|---|
steps | ("username" | "passkey" | "recover")[] | optional | all three | Which steps this surface offers. ["passkey"] offers the passkey after a redirect sign-in and nothing else. A step you leave out is not shown and the sign-in simply completes. |
presentation | "modal" | "drawer" | optional | "modal" | How it opens. |
drawerSide | "bottom" | "left" | "right" | optional | "bottom" | Drawer anchor edge. |
width | number | string | optional | 420 | Card / drawer width. |
dismissable | boolean | optional | true | Escape and backdrop close it. Closing means "not now": the sign-in completes without the step. |
navigate | boolean | optional | false | The person is already on your page, so it does not navigate by default. Set true to follow redirectAfterSignIn. |
redirectAfterSignIn | string | optional | none | Where to go when navigate is true. Checked: your origin or an origin registered for the app, else /. |
onResult | (r: ElvixSignInResult) => void | optional | none | Fires once, at the true end of the step (after the provider has refreshed, so useElvixSession() is already authenticated). |
onAuthenticated | (r: { ok: true; redirect?: string; token?: string }) => void | optional | none | Same moment, the legacy shape. |
What the host sees meanwhile
- `useElvixSession()` stays anonymous until the step is done. The person is signed in on the elvix side but has not finished signing in. A header that flips to signed in, or a redirect that fires on authenticated, would otherwise tear the form down before the step shows.
- Finishing refreshes the provider before `onResult` fires, so
useElvixSession()anduseElvixUser()are authenticated inside your callback. - Closing it means not now. The sign-in completes without the step, the host sees the user, and the step is offered again at their next sign-in.
- A forged link does nothing. The return is accepted only with the one-time
statethis tab started the sign-in with (see Redirect-OAuth round-trip); a link without it opens nothing and signs nobody in.
Choosing the steps
steps={["passkey"]} is the common case: offer a passkey after Google or GitHub, never interrupt for anything else. A step that is not in the list is skipped and the sign-in completes. Leave "recover" in unless your own flow handles it: it asks whether to restore a deactivated or left membership, which is a decision rather than an upsell.
The passkey step appears only when the person has no working passkey for this app. A passkey made for another app does not count, and neither does an account-level one that has never signed anyone in. A person who already has one signs in and is not asked.
A modal sign-in form already does this
<ElvixSignInForm presentation="modal" | "drawer"> opens itself on a redirect return that carries a step (resumeOnReturn, default on), so if your sign-in is already that form you need nothing else. Mount <ElvixOnboarding> when your sign-in surface is not the form (a hosted redirect page, your own buttons) or you want the post-redirect step offered everywhere in the app.
Building your own surface
useElvixOnboarding() returns { pending, step }. Open any surface you like while pending is true: render <ElvixSignInForm> controlled with resumeOnReturn={false}, as below. The form finishes the step in place.
"use client";import { ElvixSignInForm, useElvixOnboarding } from "@elvix.is/sdk/react";// Your own surface: open it whenever a step is waiting.export function MySignInModal() {const { pending, step } = useElvixOnboarding(); // step: "username" | "passkey" | "recover" | nullreturn (<ElvixSignInFormpresentation="modal"open={pending}resumeOnReturn={false}onResult={(r) => r.ok && location.reload()}/>);}
Do not navigate from a handler that fires when the session turns authenticated while pending is true: it cannot, by design, but a redirect on the elvix:return-token event can. Let the form finish; onResult is the true end. Requires @elvix.is/sdk 0.14.3 or later, and an elvix that echoes the state (live since 2026-10-12).
Related
ElvixSignInForm
The full branded sign-in surface (card, header, brand wash, every Console-enabled method, Secured-by-elvix badge). Exported as both `ElvixSignInForm` and the short alias `ElvixSignIn` — same component.
ReadElvixProvider
Top-level context. Fetches the brand chord, factor config, and copy from elvix; pipes session state and the event channel to every nested <Elvix*> component.
ReadElvixAppPasskeys
Per-app passkey manager. Lists and lets the user add or remove WebAuthn passkeys scoped to ONE application — the credential can only sign them in to this app. Account-level passkeys (added on `/account/security`) work everywhere and are NOT shown here.
Read