> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userkit.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Components by script

> Sign-in, account, pricing and billing on a page with no build — the whole SDK in one line of HTML.

**It is the shortest way to put UserKit on a page**, and it works in any stack:
Rails, WordPress, Django, static HTML, a landing page somebody edits in a CMS. No
npm, no build, no React on your side.

```html theme={null}
<div data-userkit="UserButton"></div>

<script
  src="https://cdn.userkit.dev/userkit.js"
  data-publishable-key="uk_pk_live_…"
  defer
  crossorigin="anonymous"
></script>
```

The element says **which** component; the script says **whose** page this is.
There is nothing else to write.

If your front end already has a build, the `@userkit/react` package
ships the same components as code — with types, SSR and composition. Both draw the
same thing; the difference is what you already have set up.

<Note>
  **If your page already loads `widget.js`, it already mounts these components** —
  same markup, same tag, no second `<script>` line. `userkit.js` is for pages that
  want the components and **no** floating button in the corner: a sign-in screen, a
  pricing page. What each one downloads is still only what the page uses — the help
  panel does not come along here, and the components app is fetched over there only
  on the first mount.

  Both on one page is fine in **either order**: whichever runs first builds the
  client, and the other adopts it — one session, one event queue, one set of page
  views. Give both tags the **same `data-publishable-key`**; one page is one
  environment, and a tag naming a second key says so in the console and does
  nothing.
</Note>

## The names are the package's names

`data-userkit="UserButton"` mounts what `<UserButton />` renders, and the `data-*`
are its props. That is deliberate: the day you adopt a build, migrating is rewriting markup
as JSX with **the same names** — no API to relearn.

| `data-userkit`        | What appears                                                                                                                                                  |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `UserButton`          | The avatar with the account menu                                                                                                                              |
| `AccountSettings`     | Profile, devices, teams                                                                                                                                       |
| `CustomerSwitcher`    | Switch team                                                                                                                                                   |
| `CustomerProfile`     | Administer the current team                                                                                                                                   |
| `PricingTable`        | Your catalogue's plans and prices                                                                                                                             |
| `Billing`             | Subscription, invoices and the provider portal                                                                                                                |
| `Credits`             | The team's credit balance, and the statement                                                                                                                  |
| `Referrals`           | This person's referral code                                                                                                                                   |
| `NotificationBell`    | The bell and their feed. `data-variant="icon"` draws the glyph with a dot — no border, no count; `UserKit.notifications.show()` opens the panel from anywhere |
| `ChangelogBadge`      | Unread release notes                                                                                                                                          |
| `WhatsNew`            | The "while you were away" dialog, which opens itself                                                                                                          |
| `FeedbackButton`      | The ideas board                                                                                                                                               |
| `OnboardingChecklist` | The onboarding checklist                                                                                                                                      |
| `OnboardingLauncher`  | The inline button that reopens a guide somebody closed                                                                                                        |
| `Survey`              | The outstanding survey, if there is one                                                                                                                       |
| `Banner`              | The announcement published to a slot — needs `data-slot`                                                                                                      |
| `HelpWidget`          | The help panel, here in the page's flow                                                                                                                       |
| `SupportChat`         | The same panel, under its older name                                                                                                                          |
| `Agreements`          | The terms this person still has to accept                                                                                                                     |
| `HelpMarkdown`        | Markdown as elements — needs `data-source`                                                                                                                    |
| `SignedIn`            | Shows what is inside it only to somebody with a session                                                                                                       |
| `SignedOut`           | The other way round — and neither draws while loading                                                                                                         |
| `SessionLoading`      | The skeleton, while the session is being restored                                                                                                             |
| `Verified`            | Only for a session whose identity was **proven**                                                                                                              |
| `Flag`                | Only while a flag is on — needs `data-name`                                                                                                                   |

**This table is the whole package.** Every component `@userkit/react` exports is
here, and a test in the repository is what keeps that true — what exists with a build
exists without one, under the same name.

## The five that draw YOUR markup

`UserButton` is handed an empty element and fills it. The five guards are the opposite:
what is inside the element is exactly what they decide whether to show.

```html theme={null}
<div data-userkit="SignedOut">
  <a href="/sign-in">Sign in</a>
</div>

<div data-userkit="Flag" data-name="new_checkout">
  <a href="/checkout-v2">Check out</a>
</div>
```

They are your nodes, not a copy of them: the script takes the element's children out
before mounting and hands the same nodes back inside the guard. An
`addEventListener` your page had already attached to them still works.

When the answer is no they **leave the page** rather than being hidden — which
matters, because hidden markup is still readable by anyone who opens the inspector.
And since `<Flag>`'s `fallback` is markup, and an attribute carries none, the HTML
form has no such prop: write the old path into a second element with the flag
inverted, or use `UserKit.mount()`, which takes real JavaScript.

## Props as attributes

`data-redirect-uri` becomes `redirectUri`; `data-customer="false"` becomes
`customer: false`; `data-poll-interval-ms="30000"` becomes a number. Those three
conversions are all of them.

```html theme={null}
<div data-userkit="AccountSettings" data-customer="true" data-devices="false"></div>
<div data-userkit="Banner" data-slot="dashboard-top"></div>
<div data-userkit="Billing" data-portal-return-url="/account"></div>
```

A date stays text: `data-since="2026-01-01"` arrives as a string, not as `2026`.

## When something happens

An attribute cannot hold a function, so the values a page wants to act on
arrive as DOM events, from the element itself:

```html theme={null}
<div id="user" data-userkit="UserButton"></div>
<script>
  document.getElementById("user").addEventListener("userkit:signedout", () => {
    location.href = "/";
  });
</script>
```

| Event                   | From           | `detail`          |
| ----------------------- | -------------- | ----------------- |
| `userkit:signedout`     | `UserButton`   | `null`            |
| `userkit:priceselected` | `PricingTable` | `{ price, plan }` |

They bubble, so a listener on `document` catches them too. Signing in is not
an event here because it is not something these components do: your own auth
signs the person in, and `UserKit.boot({ externalId, hash })` is the call that
tells the page who they are.

## When HTML is not enough

Props that do not fit in an attribute — a list, an object, a function — go through
the global API, which exists as soon as the script loads:

```html theme={null}
<script src="https://cdn.userkit.dev/userkit.js"
        data-publishable-key="uk_pk_live_…" data-auto="false" defer crossorigin="anonymous"></script>
<script>
  UserKit.mount("#pricing", "PricingTable", {
    onSelected: (price, plan) => analytics.track("price_selected", { plan: plan.key }),
  });
</script>
```

| Call                                  | What it does                                                                                    |
| ------------------------------------- | ----------------------------------------------------------------------------------------------- |
| `UserKit.mount(target, name, props?)` | Mounts one component. `target` is a selector or the element                                     |
| `handle.update(props)`                | Redraws the mounted component with different props, without remounting                          |
| `handle.unmount()`                    | Takes this component off the page, even before it has appeared                                  |
| `UserKit.mountAll()`                  | Mounts every `[data-userkit]` not already mounted — useful after swapping the DOM               |
| `UserKit.destroy()`                   | Takes them all off the page                                                                     |
| `UserKit.boot(...)`                   | Identifies the visitor, same as the SDK                                                         |
| `UserKit.ready()`                     | Promise resolving with the client, session already restored — or `null` with no publishable key |
| `UserKit.setTheme(...)`               | `"light"`, `"dark"` or `"system"` for everything mounted                                        |
| `UserKit.client`                      | The client, for what the components do not do                                                   |

`data-auto="false"` turns off automatic mounting, for a page that would rather
decide when.

### What `mount()` hands back

The components arrive over the network, so nothing is on screen when the call
returns. The object it hands back answers both things you want from that: you
can **await** it, and you can **use it before**.

```js theme={null}
const banner = UserKit.mount("#notice", "Banner", { slot: "dashboard-top" });

banner.update({ slot: "billing" });  // redraws; not a second mount
banner.unmount();                    // works even before the component appears

const handle = await banner;         // the handle once it is on screen, or null
```

`update()` **replaces** the props — it does not merge with the previous ones —
and it is a re-render rather than a remount: a half-filled form survives it. It
is also how a function passed as a prop stays current, because handlers are read
on every draw instead of being frozen at the first one.

`unmount()` called before the bundle arrives cancels the mount. That matters to
anyone mounting from inside a framework: without it, a component that leaves the
screen while the bundle is still in flight would strand a mount in a node the
framework has already dropped.

<Note>
  An element handed to `mount()` is **claimed immediately**, so the automatic
  scan will not mount over it with its own `data-*`. Even so, code that mounts
  does not need to mark the element with `data-userkit`: the name is already in
  the call, and marking both is asking two halves to describe the same thing.
</Note>

### Knowing when the tag has loaded

The tag is `defer`red, so your page has no way of knowing when our file ran.
Both loaders announce themselves when they install `window.UserKit`. Check for
the object **before** you listen, and there is no moment to miss:

```js theme={null}
function go(uk) { uk.mount("#notice", "Banner") }

if (window.UserKit) go(window.UserKit);
else document.addEventListener("userkit:load", (e) => go(e.detail), { once: true });
```

`detail` is the object itself. On a page carrying **both** files the event fires
twice: `widget.js` widens the object `userkit.js` installed, and the second time
is a real change to what it can do. `{ once: true }` is right for a listener that
wants `mount`, which both files have, and wrong for one that wants `show`.

## React without the components in your bundle

A React app loading the script from the CDN gets both halves: the React is
yours, and the components still arrive from the CDN on demand. The
[`@userkit/react-mount`](https://www.npmjs.com/package/@userkit/react-mount)
package is the bridge — it draws nothing itself, and imports nothing at runtime
but React.

```bash theme={null}
npm i @userkit/react-mount
```

```tsx theme={null}
import { Banner, OnboardingChecklist } from "@userkit/react-mount";

<OnboardingChecklist props={{ slot: "dashboard", onStepAction: go }} />
<Banner props={{ slot: "billing" }} />
```

It waits for the tag on its own, hands over new props as a re-render rather than
a remount, reads callbacks on every render, and cancels the mount if the
component leaves the screen before the bundle arrives.

<Note>
  If your app already bundles React and does not mind bundling the components
  too, `@userkit/react` is more direct — real
  components, with typed props. This package is for when you specifically want
  the heavy drawing to stay on the CDN.
</Note>

The types for `window.UserKit` are published in `@userkit/js/embed`, which has
no runtime half — install it as a devDependency and import with `import type`:

```ts theme={null}
import type { UserKitEmbed } from "@userkit/js/embed";
```

### Mounting `@userkit/react` inside a shadow root

If you render the npm components into a shadow root of your own, wrap that tree
in `<MountTargetProvider>` so the stylesheet lands inside the boundary and the
floating halves portal inside it too — neither crosses a shadow boundary on its
own:

```tsx theme={null}
import { MountTargetProvider, UserKitProvider, UserButton } from "@userkit/react";

<MountTargetProvider value={{ styles: shadowRoot, portal: container }}>
  <UserKitProvider publishableKey="uk_pk_live_…">
    <UserButton />
  </UserKitProvider>
</MountTargetProvider>
```

It replaces `setMountTarget({ styles, portal })`, which was page-wide: one page
can hold two trees — the help panel in its shadow root and a drop-in in your own
layout — and a single setting meant the last mount decided for both, so a
component mounted after the panel opened drew with no stylesheet and portalled
into the panel's boundary. Passing `styles` or `portal` to `setMountTarget` now
logs a warning and does nothing; the function still sets the CSP `nonce`, which
is genuinely page-wide.

## Appearance

The widget's tokens, on the script:

```html theme={null}
<script src="https://cdn.userkit.dev/userkit.js"
        data-publishable-key="uk_pk_live_…"
        data-color-primary="#0b5cff"
        data-border-radius="0.75rem"
        data-theme="light"
        data-locale="en"
        defer crossorigin="anonymous"></script>
```

`data-theme` is `light`, `dark` or `system` (the default), and it is what answers
an app that decides its own theme: without it the components follow the reader's
`prefers-color-scheme`, which disagrees with the toggle in your header on the night
the OS is dark and the person chose light.

It has two levels here, because the questions differ. On the script it is the
page's; on an element it is that one component's, and **the element wins** — a
`<PricingTable />` inside a dark section of an otherwise light page is a decision
made by whoever wrote that div:

```html theme={null}
<div data-userkit="PricingTable" data-theme="dark"></div>
```

With the page up, `UserKit.setTheme("dark")` (or `"light"`, or `"system"`) redraws
every mounted component — without remounting, so a half-filled form survives the
switch. An element with a `data-theme` of its own keeps it.

The typeface **inherits the page's** by default, and that is deliberate: unlike the
floating widget, these do not live inside a shadow root. You place them inside your
own layout, so they should look like part of it — your CSS reaches the `.uk-*`
classes, which is exactly what the npm package offers anybody with a build.

## What the page pays

| File                 | Size          | When                                                                     |
| -------------------- | ------------- | ------------------------------------------------------------------------ |
| `userkit.js`         | \~11 KB gzip  | always                                                                   |
| `v/vendor-<hash>.js` | \~115 KB gzip | when there is a first mount point — React, the client and the stylesheet |
| `u/app-<hash>.js`    | \~16 KB gzip  | with it — the components themselves                                      |

A page that loads the script and marks nothing downloads no React. The hashed
files carry a hash of their contents and are cached for a year; the first never
changes address and is revalidated within the hour, so a fix reaches your page
on a visitor's next navigation without you deploying anything. The shared file
is the same one the help widget uses, so a page carrying both downloads React
once.

## Content Security Policy

```
script-src  'self' https://cdn.userkit.dev;
connect-src 'self' https://api.userkit.dev;
img-src     'self' https: data:;
```

The components mount into your own DOM, and their stylesheet is moved onto a
constructed sheet (`document.adoptedStyleSheets`) the moment it is written, so
`style-src` needs nothing for it. A browser without constructed sheets, and the
dark palette set in the panel — which the components render as a `<style>` in
their own tree — still need `style-src 'unsafe-inline'`; without it the forms
draw in light.

The `crossorigin="anonymous"` on the tag is what lets an error of ours reach
your `window.onerror` with a stack rather than as `"Script error."`.

## What it never does

It creates no contact on its own. `boot()` is your decision here as it is in the
package — a `<script>` that quietly started creating contacts from landing-page
traffic would be the worst possible default.
