Components
ElvixOnboarding

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.

A
Sign in faster next time
Touch ID, Face ID, or your security key. No more email codes.
  • No more password or email codes
  • Phish-proof. Works only on this site.
  • Syncs across your devices via iCloud, Google, or 1Password
By continuing, you agree to Acme's
v0.19.0
components/providers.tsx
"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 redirect
sign-in brings back a step, then opens itself on that step. */}
<ElvixOnboarding
steps={["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

PropTypeRequiredDefaultDescription
steps("username" | "passkey" | "recover")[]optionalall threeWhich 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.
widthnumber | stringoptional420Card / drawer width.
dismissablebooleanoptionaltrueEscape and backdrop close it. Closing means "not now": the sign-in completes without the step.
navigatebooleanoptionalfalseThe person is already on your page, so it does not navigate by default. Set true to follow redirectAfterSignIn.
redirectAfterSignInstringoptionalnoneWhere to go when navigate is true. Checked: your origin or an origin registered for the app, else /.
onResult(r: ElvixSignInResult) => voidoptionalnoneFires 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 }) => voidoptionalnoneSame 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() and useElvixUser() 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 state this 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.

components/my-sign-in-modal.tsx
"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" | null
return (
<ElvixSignInForm
presentation="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