---
title: Components
description: The drop-in UI, and how it picks up your brand.
---

import Callout from '../../../components/Callout.astro';
import { ComponentPreview } from '../../../previews/ComponentPreview';


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.

<Callout type="info">
Every preview on this page is the real component, running in your browser
against fixture data. Nothing has a key and no request leaves the page, so the
buttons do nothing. Use the light and dark switch to see what your users get.
</Callout>

## SignIn

```tsx
<SignIn afterSignInUrl="/dashboard" />
```

<ComponentPreview name="sign-in" client:load />

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:

<ComponentPreview name="sign-in-all-methods" client:load />

With several enabled that gets long. `secondaryMethods="collapsed"` folds the
alternatives behind a toggle:

```tsx
<SignIn secondaryMethods="collapsed" />
```

<ComponentPreview name="sign-in-collapsed" client:load />

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.

```tsx
<SignInWithZevId />
```

<ComponentPreview name="sso-button" client:load />

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

```tsx
<SignUp afterSignUpUrl="/welcome" />
```

<ComponentPreview name="sign-up" client:load />

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

```tsx
<UserButton afterSignOutUrl="/" />
```

<ComponentPreview name="user-button" client:load />

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

```tsx
<UserProfile />
```

<ComponentPreview name="user-profile" client:load />

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:

```tsx
<UserProfile section="security" />
```

<ComponentPreview name="user-profile-security" client:load />

| 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

```tsx
<OrganizationSwitcher />
```

<ComponentPreview name="organization-switcher" client:load />

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

```tsx
<OrganizationProfile />
```

<ComponentPreview name="organization-profile" client:load />

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`.

```tsx
<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:

<ComponentPreview name="sign-in" accent="#2563eb" client:load />
 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.

<Callout type="info">
`colorScheme: system`, the default, follows the viewer's own light and dark
preference. A sign-in box blazing white inside somebody's dark application
looks like it came from a different site, which is an instinct we would rather
not dull.
</Callout>

## 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.