---
title: Any framework
description: The drop-in components for Svelte, Angular, Astro, server-rendered pages and plain HTML, with or without a build step.
---

`@zevauth/ui` puts the ZevAuth components on any page, whatever renders the
rest of it. It works the same in Svelte, Angular, Solid, a Rails or Django
view, or an HTML file with a script tag.

If your app is React, use [`@zevauth/react`](/sdk/react/) instead. If it's
Vue or Nuxt, use [`@zevauth/vue`](/sdk/vue/), which wraps this package.

## Install

```bash
npm install @zevauth/ui
```

```ts
import { createZevAuthUI } from '@zevauth/ui';

export const zevauth = createZevAuthUI({
  publishableKey: import.meta.env.VITE_ZEVAUTH_KEY,
});
```

Create the instance once, in a module of its own, and import it wherever
you mount a component. Every component from one instance shares one client
and one session.

## Without a build step

```html
<div id="sign-in"></div>

<script src="https://cdn.jsdelivr.net/npm/@zevauth/ui@0.1.0/dist/zevauth-ui.global.js"></script>
<script>
  ZevAuthUI.init({ publishableKey: 'pk_live_...' });
  ZevAuthUI.mountSignIn('#sign-in', { afterSignInUrl: '/dashboard' });
</script>
```

`init` creates one page-wide instance; calling it again returns the same
one. The `mount…` functions on `ZevAuthUI` use it. Pin the version in the
URL so a release you haven't tested never reaches your users.

## Mounting

Every `mount…` function takes an element, or a selector for one, and the
same props as the React component of the same name:

| Function | Component |
| --- | --- |
| `mountSignIn` | [`<SignIn>`](/sdk/components/#signin) |
| `mountSignUp` | [`<SignUp>`](/sdk/components/#signup) |
| `mountSignInWithZevId` | [`<SignInWithZevId>`](/sdk/components/#signinwithzevid) |
| `mountUserButton` | [`<UserButton>`](/sdk/components/#userbutton) |
| `mountUserProfile` | [`<UserProfile>`](/sdk/components/#userprofile) |
| `mountOrganizationSwitcher` | [`<OrganizationSwitcher>`](/sdk/components/#organizationswitcher) |
| `mountOrganizationProfile` | [`<OrganizationProfile>`](/sdk/components/#organizationprofile) |

Each returns a handle:

```ts
const signIn = zevauth.mountSignIn('#sign-in', { afterSignInUrl: '/dashboard' });

signIn.update({ afterSignInUrl: '/welcome' }); // merged over the current props
signIn.unmount();                              // the element is left empty
```

Mounting into an element that already holds a component replaces it. That
makes it safe to call from code that runs more than once, such as a
framework's mount hook in development.

The element has to be in the page when you mount. A selector that matches
nothing throws an error naming the selector.

Props that take React elements, such as `aside` and `children` on
`<SignIn>`, are React-only. Everything else, including `appearance` and
`localization`, passes straight through.

## Appearance and wording

Give them to the instance and every component follows. Give them to one
mount and only that component changes:

```ts
const zevauth = createZevAuthUI({
  publishableKey: 'pk_live_...',
  appearance: { theme: 'soft', variables: { colorPrimary: '#4f46e5' } },
  localization: { signIn: { submit: 'Se connecter' } },
});

zevauth.mountUserButton('#account', { appearance: { elements: { avatar: 'ring-2' } } });

// Later, for everything already on the page, e.g. after a theme switch:
zevauth.update({ appearance: { colorScheme: 'dark' } });
```

The options are the same as in React. See
[Customising the components](/concepts/appearance/).

## Calling your own API

`zevauth.client` is the client every component uses. Ask it for a token and
send it as a bearer token:

```ts
const token = await zevauth.client.getToken();

await fetch('/api/projects', {
  headers: { Authorization: `Bearer ${token}` },
});
```

With the script-tag build, `ZevAuthUI.getClient()` returns the page-wide
client. Verify the token on your server with
[`@zevauth/backend/verify`](/concepts/verifying-tokens/).

Listen for sign-in and sign-out to update the rest of your page:

```ts
zevauth.client.addListener(({ isLoaded, user }) => {
  document.body.dataset.signedIn = String(isLoaded && user !== null);
});
```

> **Warning:**
> Use the client from `zevauth.client`, or create one with `createZevAuth`
> imported from `@zevauth/ui`. A client created from `@zevauth/js` directly is
> a second copy of the package. The components don't recognise its errors, so
> they show "Something went wrong" instead of the real reason.

## In your framework

Mount when the framework has put the element in the page, and unmount when
it takes it away.

**Svelte**

```svelte
<script>
  import { onMount } from 'svelte';
  import { zevauth } from '$lib/zevauth';

  let element;
  onMount(() => {
    const signIn = zevauth.mountSignIn(element, { afterSignInUrl: '/dashboard' });
    return () => signIn.unmount();
  });
</script>

<div bind:this={element}></div>
```

**Angular**

```ts
@Component({ selector: 'app-sign-in', template: '<div #host></div>' })
export class SignInComponent implements AfterViewInit, OnDestroy {
  @ViewChild('host') host!: ElementRef<HTMLElement>;
  private mounted?: { unmount(): void };

  ngAfterViewInit() {
    this.mounted = zevauth.mountSignIn(this.host.nativeElement);
  }
  ngOnDestroy() {
    this.mounted?.unmount();
  }
}
```

**Astro**: see [Astro](/sdk/astro/).

## What's inside

The components are built with React, and this package carries its own
private copy. Your page doesn't need React installed, and a React version
elsewhere on the page can't conflict with it.

That copy is most of the package's size: about 87 kB gzipped. A React app
already has React, which is why it should use `@zevauth/react` and
download none of this twice.

| Export | What it is |
| --- | --- |
| `createZevAuthUI(options)` | An instance: `client`, the `mount…` functions, `update`, `destroy` |
| `init(options)` | The page-wide instance, created once |
| `mountSignIn` … `mountOrganizationProfile` | Mount with the page-wide instance |
| `getClient()` | The page-wide instance's client |
| `createZevAuth`, `ZevAuthError` | The client factory and error class, from the same copy the components use |

`options` takes `publishableKey` (or a `client`), `apiUrl`, `appearance` and
`localization`.
