Building with the SDK
Components
The drop-in UI, and how it picks up your brand.
Every component renders what your environment has enabled. Turn magic links on in the console and they appear; turn passkeys off and they stop being offered. You do not redeploy.
SignIn
<SignIn afterSignInUrl="/dashboard" />
Renders whichever of password, magic link, email code and ZevID your environment accepts. Enable more in the console and they appear here, with no change to your code:
With several enabled that gets long. secondaryMethods="collapsed" folds the
alternatives behind a toggle:
<SignIn secondaryMethods="collapsed" />
Which is right depends on the method you want people to reach for, so it is yours to choose rather than ours to decide.
If none are enabled it says so, rather than rendering an empty box.
| Prop | Type | Default | |
|---|---|---|---|
afterSignInUrl | string | Where to go once signed in. Omit it and the component simply stops rendering the form, leaving your router in charge. | |
secondaryMethods | 'inline' | 'collapsed' | 'inline' | Whether the passwordless alternatives are laid out or folded away. |
children | ReactNode | Rendered instead of the form once somebody is signed in. | |
className | string | Applied to the outer wrapper. |
SignInWithZevId
Just the SSO button, for a sign-in page you built yourself.
<SignInWithZevId />
No card, no identity block, no attribution: it is a control inside your layout
rather than a screen of ours. You do not have to adopt <SignIn> to offer
ZevID.
| Prop | Type | Default | |
|---|---|---|---|
redirectUrl | string | current page | Where to return after signing in. |
label | string | 'Continue with ZevID' | |
variant | 'primary' | 'secondary' | 'secondary' | secondary suits a row of provider buttons. |
className | string |
Leave afterSignInUrl off and the component simply stops rendering the form
once somebody signs in, so your own router decides what happens next.
SignUp
<SignUp afterSignUpUrl="/welcome" />
If your environment requires email confirmation, this shows the “check your email” state instead of signing the person in.
| Prop | Type | Default | |
|---|---|---|---|
afterSignUpUrl | string | Where to go once the account exists and a session started. | |
children | ReactNode | Rendered instead of the form once signed in. | |
className | string |
UserButton
<UserButton afterSignOutUrl="/" />
An avatar and a menu. Renders nothing when nobody is signed in, so it can sit beside a sign-in button in your header without the two fighting.
| Prop | Type | Default | |
|---|---|---|---|
afterSignOutUrl | string | Where to go after signing out. | |
className | string |
UserProfile
<UserProfile />
Your profile
Picture, name and password, for a settings page. The email is shown but not editable: changing an address has to be proved before it takes effect, so it is a flow of its own rather than a text input.
The picture saves on its own rather than waiting for the form’s button. By the time it appears on screen it has already been uploaded, and a Save that seemed to be needed afterwards would ask for a second commitment to something already done.
The password fields start closed. Most visits to a profile page are to change a name, and two empty password boxes on arrival invite a password manager to fill them and a person to wonder what is wrong.
Render one half at a time if your settings area has its own navigation:
<UserProfile section="security" />
| Prop | Type | Default | |
|---|---|---|---|
section | 'all' | 'profile' | 'security' | 'all' | Render both, or one, so they can live on separate pages. |
defaultPasswordOpen | boolean | false | Whether the password fields start open. |
className | string |
OrganizationSwitcher
<OrganizationSwitcher />
Hides itself for somebody who belongs to no organizations, because a switcher with one option cannot do anything.
| Prop | Type | Default | |
|---|---|---|---|
allowPersonal | boolean | true | Offer a way back to personal scope. |
className | string |
OrganizationProfile
<OrganizationProfile />
Northwind
Lists members and, for anybody with org:members:manage, offers the controls
to change them. The organization’s picture appears above them for anybody with
org:manage, which is a separate permission on purpose: inviting a colleague
and renaming the company are not the same authority.
| Prop | Type | Default | |
|---|---|---|---|
className | string |
ImageField
The picture control from <UserProfile> and <OrganizationProfile>, exported
on its own so a custom settings screen gets the same behaviour rather than
reimplementing the upload-then-save sequence around uploadAvatar.
<ImageField
label="Photo"
value={user.imageUrl}
upload={(file, onProgress) => client.me.uploadAvatar({ file, onProgress })}
onSave={(imageUrl) => client.me.update({ imageUrl })}
/>
upload and onSave are separate so you decide what the URL is saved to. That
is what lets the same control serve a user’s avatar and an organization’s
image without knowing which it is looking at.
| Prop | Type | Default | |
|---|---|---|---|
label | string | ||
value | string | null | The picture now, or null. | |
upload | (file, onProgress) => Promise<string> | Resolves to the URL to save. | |
onSave | (url: string | null) => Promise<unknown> | Called with null on removal. | |
shape | 'circle' | 'square' | 'circle' | Round for a person, square for an organization. |
isDisabled | boolean | false |
Styling
The components bring their own CSS, injected once on first render. There is no stylesheet to import. That is one less line to remember, and one less thing to break in a bundler that does not handle CSS imports.
Everything is prefixed zv- and nothing styles bare elements, so the styles
cannot reach your markup and yours cannot reach ours.
Your brand arrives from the API: the accent, logo, radius and font you set in the console are applied as CSS variables. Here is the same component with a developer’s own colour:
The accent is contrast-corrected server-side for light and dark surfaces, so a colour that is beautiful on white does not become invisible on near-black. The console preview, the hosted pages and these components all paint the same value.
The ZevAuth badge
“Secured by ZevAuth” appears under the card. Whether it can be removed is decided by your plan, on the server. A client cannot turn it off. Development always shows it, so you never design against a screen production will not give you.
Updated at, Friday, August 28, 2026