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

# Ayrshare Connect Widget

> Let your users link social accounts from inside your own dashboard, with one script tag and our buttons embedded in your page.

export const PlansAvailable = ({plans = [], maxPackRequired}) => {
  let displayPlans = plans;
  if (plans && plans.length === 1) {
    const lowerCasePlan = plans[0].toLowerCase();
    if (lowerCasePlan === "business") {
      displayPlans = ["Launch", "Business", "Enterprise"];
    } else if (lowerCasePlan === "premium") {
      displayPlans = ["Premium", "Launch", "Business", "Enterprise"];
    }
  }
  return <Note>
Available on {displayPlans.length === 1 ? "the " : ""}
{displayPlans.join(", ").replace(/\b\w/g, l => l.toUpperCase())}{" "}
{displayPlans.length > 1 ? "plans" : "plan"}.

{maxPackRequired && <span onClick={() => window.open('https://www.ayrshare.com/docs/additional/maxpack', '_self')} className="flex items-center mt-2 cursor-pointer">
 <span className="px-1.5 py-0.5 rounded text-sm" style={{
    backgroundColor: '#C264B6',
    color: 'white',
    fontSize: '12px'
  }}>
   Max Pack required
 </span>
</span>}
</Note>;
};

<PlansAvailable plans={["business"]} maxPackRequired={true} />

The Ayrshare Connect widget puts our linking buttons **inside your own dashboard**. You load one
script, drop a slot wherever a network belongs in your layout, and we render a button there that
already shows whether the account is connected. Your user clicks it and links the account without
leaving your page — at most one popup, the network's own.

You write no popup handling, no OAuth callbacks, no session refresh, and no per-network logic. When
a network changes something on their end, the fix ships inside our frames the moment we deploy it;
you redeploy nothing.

## Which Surface You Want

| Surface                                                                                                             | Your user sees                                                          | White-labelling                                                                 | Choose it when                                                                                |
| ------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| **Widget — embedded frames** ([`mount()`](#mount-a-slot))                                                           | our buttons inline in your own layout; no popup until the network's own | **Strongest.** Your page, your fonts and colours, and your user never leaves it | you have a dashboard with a row per network and want linking to happen in place. Max Pack.    |
| **Widget — your own button** ([`popup()`](#your-own-button))                                                        | your button, then one popup for the network                             | **Strong.** The popup is ours, but it is brief and inherits your appearance     | you want your own button and styling, and no frames in your layout. Max Pack.                 |
| **Hosted linking page** ([how to](/docs/multiple-users/api-integration-business#single-sign-on-with-jwt-authentication)) | a page we host, carrying your logo, colours and custom CSS              | **Weakest.** It is our page, and your user leaves yours to use it               | you want one link to hand out, or you are onboarding by email. Nothing to build, no Max Pack. |

<div className="my-8">
  <Frame caption="Embedded frames: our tiles rendered inside your own layout, one slot per network or one slot for several.">
    <img src="https://mintcdn.com/ayrshare-docs/-2tpx3mFy9vWIObd/images/multiple-users/connect-widget-frames.webp?fit=max&auto=format&n=-2tpx3mFy9vWIObd&q=85&s=d6c26228a418421772f753f31b0dfc59" alt="A customer dashboard with Ayrshare Connect tiles for Instagram, TikTok and LinkedIn embedded in a card" width="2400" height="1120" data-path="images/multiple-users/connect-widget-frames.webp" />
  </Frame>
</div>

<div className="my-8">
  <Frame caption="Your own button: you render the button, and one brief popup of ours handles the network.">
    <img src="https://mintcdn.com/ayrshare-docs/-2tpx3mFy9vWIObd/images/multiple-users/connect-widget-popup.webp?fit=max&auto=format&n=-2tpx3mFy9vWIObd&q=85&s=b68021d477703d97a5994ee85d299be6" alt="A customer dashboard with its own Connect buttons and an Ayrshare popup showing the Facebook hand-off screen" width="2880" height="1317" data-path="images/multiple-users/connect-widget-popup.webp" />
  </Frame>
</div>

<div className="my-8">
  <Frame caption="Hosted linking page: a page we host, carrying your logo and colours, that your user leaves your app to use.">
    <img src="https://mintcdn.com/ayrshare-docs/-2tpx3mFy9vWIObd/images/multiple-users/connect-widget-hosted.webp?fit=max&auto=format&n=-2tpx3mFy9vWIObd&q=85&s=4d22a992f6e70808fd9ad9f065ed3879" alt="The hosted social linking page showing every available network" width="2336" height="1906" data-path="images/multiple-users/connect-widget-hosted.webp" />
  </Frame>
</div>

<Note>
  The two widget rows are **one integration**, not two. A single `init` gives you both: mount frames
  where you want our buttons, and call `popup()` from your own button everywhere else. They share
  one session and report on the same handlers.

  [Direct mode](/docs/multiple-users/connect-direct-mode) is the same popup **without** our script — for
  a page with a strict Content-Security-Policy, a server-rendered page, or a native app. There you
  open and watch the popup yourself.
</Note>

## Add the Script

Pin a version and its hash, or track a channel without one. Never both — an `integrity` attribute
on a moving URL stops working at our next release, because the file it names has legitimately
changed.

```html Pinned version theme={"system"}
<script
  src="https://app.ayrshare.com/ayrshare-connect/1.2.0/widget.js"
  integrity="sha384-qQ/of8CnYj26nF9RkQ6qk3D1vvzWesu7mpBK6MFK2D0EmmgRJHfbDmRowFBfltmz"
  crossorigin="anonymous"
></script>
```

```html Tracking v1 theme={"system"}
<script src="https://app.ayrshare.com/ayrshare-connect/v1/widget.js" crossorigin="anonymous"></script>
```

| Path                                    | Caching             | `integrity`      |
| --------------------------------------- | ------------------- | ---------------- |
| `/ayrshare-connect/<version>/widget.js` | immutable, one year | **yes** — pin it |
| `/ayrshare-connect/v1/widget.js`        | five minutes        | no               |
| `/ayrshare-connect/latest/widget.js`    | five minutes        | no               |

**`v1` is the channel to recommend.** It picks up fixes but never crosses a breaking change.
`latest` crosses major versions by definition, so it will eventually hand your page a version whose
behavior you have not reviewed.

Every version's hash is published in
[`manifest.json`](https://app.ayrshare.com/ayrshare-connect/manifest.json), which also names what
each channel currently serves:

```json manifest.json theme={"system"}
{
  "versions": {
    "1.2.0": { "integrity": "sha384-qQ/of8CnYj26nF9RkQ6qk3D1vvzWesu7mpBK6MFK2D0EmmgRJHfbDmRowFBfltmz" }
  },
  "latest": "1.2.0",
  "channels": { "v1": "1.2.0", "latest": "1.2.0" }
}
```

Each bundle also opens with a comment naming its own version, which is the fastest way to tell us
what a page is actually running:

```js theme={"system"}
/*! ayrshare-connect 1.2.0 */
```

### Content-Security-Policy

If your page sends a Content-Security-Policy, it needs **two** entries, both naming the host you
load the script from:

```
script-src https://app.ayrshare.com;
frame-src  https://app.ayrshare.com;
```

That is the whole list. You need **no `connect-src` entry** for us — our frames reach our API from
inside themselves, not from your page — and **no entry for popups**, which are top-level windows
your policy does not govern. Both entries were measured: removing `script-src` blocks the script,
and removing `frame-src` blocks the frame.

## Start an Instance

`session` is the only required option. It is called **once per instance**, not once per mount, and
must return your backend's response to
[Create a Link Session](/docs/apis/profiles/create-link-session) with `mode: "connect"` —
`{ sessionId, token, expiresAt }` — exactly as it came back.

```javascript Your page theme={"system"}
const connect = AyrshareConnect.init({
  session: () => fetch("/my-api/ayrshare-session").then(r => r.json()),
  appearance: { "--ayr-connect-accent": "#0B7A6C" },
  maxHeight: 800,
});
```

```javascript Your backend theme={"system"}
app.get("/my-api/ayrshare-session", async (req, res) => {
  const response = await fetch("https://api.ayrshare.com/api/profiles/link-sessions", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.AYRSHARE_API_KEY}`,
      "Profile-Key": profileKeyFor(req.user),
    },
    body: JSON.stringify({ mode: "connect", origin: "https://app.example.com" }),
  });

  res.json(await response.json());
});
```

A widget session names **no** `network` — it authorizes every network your account permits, and
which ones appear is decided per mount, in your page. It does carry an `origin`, which is the one
thing that makes the frames render at all: a frame checks the page embedding it against that value
and refuses to render anywhere else.

<Warning>
  Create the session on your server. The call needs your API key, and the token it returns signs
  your user into their User Profile — treat it like a password.
</Warning>

We call `session` again before the token expires, so the widget keeps working on a page left open
all day. A call that rejects, or returns no token, is retried twice more — after 0.5s, then 1s —
before we give up and emit `error`. So one refresh costs at most three calls to your endpoint.

| Option       | Default      | Does                                                                      |
| ------------ | ------------ | ------------------------------------------------------------------------- |
| `session`    | —            | **Required.** Returns a connect-mode link session.                        |
| `appearance` | our defaults | Design tokens, applied inside every frame. See [Appearance](#appearance). |
| `css`        | none         | A CSS string applied inside every frame. See [Custom CSS](#custom-css).   |
| `maxHeight`  | `600`        | How tall a frame may grow before it scrolls internally instead.           |

## Mount a Slot

One `mount` per slot. Ask for one network, several, or everything the session permits — the
granularity is yours, so a slot can be a single row in an existing table or one panel holding
everything.

```javascript theme={"system"}
connect.mount("#instagram-slot", { network: "instagram" });
connect.mount("#some-slot", { networks: ["facebook", "tiktok", "x"] });
connect.mount("#everything");

const row = connect.mount(document.querySelector("#tall"), { maxHeight: 1200 });
row.unmount();
```

`mount` takes a CSS selector or an element, and returns `{ unmount, element }`. It throws if the
target matches nothing — which is almost always a slot that does not exist yet, so mount after your
markup is in the document.

Each mount is one iframe. It reports its own height to us and we resize it to match, so your layout
reflows as our content changes; past `maxHeight` the frame scrolls internally instead of running off
your page. **The narrowest slot we support is 300px.**

Network keys are Ayrshare's own, and the alias spellings work too: `instagram` and `instagramapi`
both mean `instagramApi`, and `x` means `twitter`. A key that is not a network renders no tile.

## Your Own Button

A customer who would rather use their own button than one of our frames calls `popup` instead. It
runs the same flow, on the same session, and reports on the same handlers.

<Warning>
  **Call it directly inside the click handler, with nothing awaited before it.** A browser only
  permits a popup while it is still processing your user's click, and that permission does not
  survive an `await`. Nothing needs awaiting anyway — the session was created at `init`.
</Warning>

```javascript theme={"system"}
linkedInButton.addEventListener("click", () => {
  connect.popup({ network: "linkedin" });
});
```

`popup` returns `{ close(), network }`, and **always** returns a handle — including after a blocked
popup, where `close()` does nothing — so your code never has to null-check it before calling
`close()`.

Every network works here, including the ones a frame finishes inside its own panel: Facebook shows
its hand-off explainer in the popup, Bluesky and X show their credential form, and LinkedIn,
Pinterest, YouTube and Google Business go out to the network and come back.

It throws synchronously for the three things that are programming errors — no `network`, a
destroyed instance, or a session that has not resolved yet. A blocked popup is **not** one of them:
that emits `error` with `reason: "popupBlocked"`, because your user did nothing wrong.

At most one popup is open at a time. A second call closes the first and reports `cancelled` with
`reason: "superseded"` on it. A popup one of our **frames** opened is a different thing and is never
touched, so your button cannot cancel a flow running inside a mounted slot.

<Note>
  Whether the session may link a network is the **server's** answer, not the script's. A session
  scoped to Bluesky that is asked for LinkedIn gets a refusal rendered in the popup and reported as
  `error`. The script only checks that a network was named at all.
</Note>

## React

The script is framework-free, so React needs nothing special from us — but four things about its
lifecycle are worth getting right the first time.

Load the script **once**, outside your component tree. In Next.js that is `next/script` in your
root layout; in Vite or Create React App it is a tag in `index.html`. Loading it per component
re-runs it on every mount.

```jsx ConnectAccounts.jsx theme={"system"}
import { useEffect, useRef, useState } from "react";

export function ConnectAccounts() {
  const slot = useRef(null);
  const [linked, setLinked] = useState([]);

  useEffect(() => {
    // Inside the effect, so the ref is attached: mount() throws if its target
    // does not exist yet, which is exactly what happens if you call it during render.
    const connect = window.AyrshareConnect.init({
      session: () => fetch("/my-api/ayrshare-session").then(r => r.json()),
      appearance: { "--ayr-connect-accent": "#0B7A6C" },
    });

    connect.mount(slot.current, { networks: ["instagram", "tiktok", "x"] });

    const stop = connect.on("success", ({ network }) => {
      setLinked(current => [...current, network]);
    });
    // `success` is one of ten events the widget reports. See Events below for
    // the full list, including `state`, which replaces polling.

    // destroy() takes the frames, the message listener and the timers with it.
    // Without this, a route change leaves all three behind.
    return () => {
      stop();
      connect.destroy();
    };
  }, []);

  return <div ref={slot} />;
}
```

<Warning>
  **Never reuse an instance after `destroy()`.** A destroyed instance stays destroyed — `popup()`
  throws on one, and `mount()` will not bring it back. Create a new instance in the next effect run,
  which is what the code above does.
</Warning>

Two consequences of that pattern, neither of them a bug:

* **In development you will see the session callback fire twice.** React's Strict Mode runs effects
  mount → unmount → mount, so the instance is created, destroyed and created again. The cleanup
  above makes that safe; it costs one extra call to your backend in dev and none in production.
* **Keep the effect's dependencies stable.** An array literal passed straight into `mount` from a
  parent render is a new value every time, so an effect that depends on it tears the widget down and
  rebuilds it on every render. Memoize it, or keep it constant as above.

## Events

Subscribe with `on`, which returns an unsubscribe function. Handlers receive the event payload and
the mount it came from; `off(name, handler)` does the same job when you would rather name the
handler.

```javascript theme={"system"}
const stop = connect.on("success", ({ network, displayName }, mount) => {
  refreshRow(network, displayName, mount.element);
});
stop();

connect.on("error", ({ network, code, message }) => report(code, message));
connect.destroy(); // every frame, listener and timer
```

Ten events. Every one carries a `network` except `ready`, and except the one kind of `error` that
is not about a network at all — see [`error` has two sources](#error-has-two-sources) below.

| Event       | Fires when                                                | Also carries                                  |
| ----------- | --------------------------------------------------------- | --------------------------------------------- |
| `ready`     | a frame is mounted and ready                              | —                                             |
| `click`     | your user clicked a network, **in either direction**      | `action`: `"connect"` or `"unlink"`           |
| `started`   | a link attempt is under way                               | —                                             |
| `selection` | your user reached a picker or a form                      | `step`                                        |
| `success`   | the account is connected **and saved**                    | `displayName` (omitted when unknown), `refId` |
| `unlinked`  | a connected account was removed, and the removal is saved | —                                             |
| `error`     | the attempt failed                                        | `code`, `message`, `reason`                   |
| `cancelled` | your user backed out                                      | `reason`                                      |
| `closed`    | the popup this attempt used has closed                    | —                                             |
| `state`     | a network's account state, at mount and on every change   | `state`, `since`                              |

**Four of them are endings** — `success`, `unlinked`, `error` and `cancelled` — and exactly one
arrives per attempt. `closed` is a lifecycle notice that follows an ending rather than being one.

`click` fires **before** any linking work starts, so it reports a click that a blocked popup or a
dead session goes on to refuse. It is the event to use for your own analytics; `started` is the one
that means an attempt is really running.

### Reasons

| Event       | `reason`        | Means                                                      |
| ----------- | --------------- | ---------------------------------------------------------- |
| `error`     | `popupBlocked`  | the browser refused to open the popup                      |
| `cancelled` | `popupClosed`   | your user closed the window by hand                        |
| `cancelled` | `scopesDenied`  | your user declined a permission the network asked for      |
| `cancelled` | `userCancelled` | your user backed out, or your code called `handle.close()` |
| `cancelled` | `superseded`    | a second `popup()` call replaced this attempt              |

### `error` Has Two Sources

Only one of them is a link failure, and they carry different fields.

* A **link error** carries `network`, `code` and the mount it came from.
* A **session error** — we could not create or refresh your session — carries only `message`,
  because nothing was being linked at the time.

Destructure defensively: `code` and `network` are `undefined` on the second kind.

### `state` Saves You Polling

`state` is the data channel rather than a report on an attempt. Every frame emits one per network as
it mounts, carrying that network's current state and the `since` timestamp it has held it from, and
another whenever a state changes — **including changes that originate on our side**, such as a token
dying into relink-required. So you can drive your whole UI from the widget without polling anything.

The values are the same enum
[`GET /profiles` with `include=state`](/docs/apis/profiles/get-profiles) returns: `linked`, `unlinked`,
`identityVerificationRequired`, `restricted`, `rateLimited`, `suspended`.

## Unlinking

Our tiles unlink as well as link. Your user clicks a connected network, confirms, and the account is
removed:

1. `click` fires with `action: "unlink"`.
2. `unlinked` fires once the removal is saved.

An unlink that **fails** reports `error`, and one your user backs out of at the confirm step reports
`cancelled`. There is no separate unlink-failed event.

## Appearance

A customer's stylesheet cannot reach inside a cross-origin frame, so styling travels as data we
apply within it. Pass `appearance` to `init` as CSS custom properties; each one we do not receive
keeps our default.

```javascript theme={"system"}
const connect = AyrshareConnect.init({
  session: getSession,
  appearance: {
    "--ayr-connect-font-family": "Inter, system-ui, sans-serif",
    "--ayr-connect-surface-bg": "#111318",
    "--ayr-connect-surface-fg": "#F2F3F7",
    "--ayr-connect-accent": "#0B7A6C",
    "--ayr-connect-accent-fg": "#FFFFFF",
    "--ayr-connect-radius": "12px",
  },
});
```

<Warning>
  **Set colors in pairs.** A background token with no foreground token beside it is the one way to
  make this produce something unreadable: set `--ayr-connect-surface-bg` to a dark value on its own
  and our default `--ayr-connect-surface-fg` is still dark navy. Nothing can infer the other half
  for you.
</Warning>

With no `appearance` at all, every token holds its default and a frame looks like this:

<div className="my-8">
  <Frame caption="Default tokens: white surfaces, dark navy text, indigo accent, 8px radius.">
    <img src="https://mintcdn.com/ayrshare-docs/-2tpx3mFy9vWIObd/images/multiple-users/connect-widget-appearance-default.webp?fit=max&auto=format&n=-2tpx3mFy9vWIObd&q=85&s=62f9ce7e061c8bb58132960b9e270f4a" alt="Three Ayrshare Connect tiles with the default appearance" width="920" height="528" data-path="images/multiple-users/connect-widget-appearance-default.webp" />
  </Frame>
</div>

Pass a handful of tokens and the same frame takes on your palette. This example turns the
surfaces pale blue, deepens the text and accent to match, and rounds the corners a little more:

```javascript theme={"system"}
const connect = AyrshareConnect.init({
  session: getSession,
  appearance: {
    "--ayr-connect-surface-bg": "#EFF6FF",
    "--ayr-connect-border-color": "#BFDBFE",
    "--ayr-connect-surface-fg": "#0F2A5F",
    "--ayr-connect-surface-fg-muted": "#4A6A9A",
    "--ayr-connect-accent": "#1D4ED8",
    "--ayr-connect-radius": "14px",
    "--ayr-connect-spacing": "10px",
  },
});
```

<div className="my-8">
  <Frame caption="The same three tiles after the tokens above are applied.">
    <img src="https://mintcdn.com/ayrshare-docs/-2tpx3mFy9vWIObd/images/multiple-users/connect-widget-appearance-themed.webp?fit=max&auto=format&n=-2tpx3mFy9vWIObd&q=85&s=686d1ad900628a6d4c75dc242f1ab38c" alt="Three Ayrshare Connect tiles restyled with pale blue surfaces and a deeper blue accent" width="920" height="574" data-path="images/multiple-users/connect-widget-appearance-themed.webp" />
  </Frame>
</div>

**There is one palette and no light/dark preset.** Nothing keys on `prefers-color-scheme`, on
purpose — your page's theme may not match your user's operating system, and a media query would
silently outvote the colors you chose. A dark dashboard is themed by supplying dark values.

This token list is a supported contract we keep across versions.

### The Tokens

| Token                              | Default                                                   |
| ---------------------------------- | --------------------------------------------------------- |
| `--ayr-connect-font-family`        | `Inter, "Segoe UI", system-ui, -apple-system, sans-serif` |
| `--ayr-connect-font-size`          | `16px`                                                    |
| `--ayr-connect-font-size-sm`       | `12px`                                                    |
| `--ayr-connect-label-font-weight`  | `600`                                                     |
| `--ayr-connect-status-font-size`   | `12px`                                                    |
| `--ayr-connect-status-font-weight` | `600`                                                     |
| `--ayr-connect-surface-bg`         | `#FFFFFF`                                                 |
| `--ayr-connect-surface-bg-hover`   | `#F7F7F7`                                                 |
| `--ayr-connect-surface-fg`         | `#010629`                                                 |
| `--ayr-connect-surface-fg-muted`   | `#56596F`                                                 |
| `--ayr-connect-border-color`       | `#DDDEE2`                                                 |
| `--ayr-connect-border-width`       | `1px`                                                     |
| `--ayr-connect-radius`             | `8px`                                                     |
| `--ayr-connect-spacing`            | `8px`                                                     |
| `--ayr-connect-accent`             | `#4553EE`                                                 |
| `--ayr-connect-accent-fg`          | `#FFFFFF`                                                 |
| `--ayr-connect-focus-ring-color`   | `#4553EE`                                                 |
| `--ayr-connect-focus-ring-width`   | `2px`                                                     |
| `--ayr-connect-disabled-fg`        | `#6E7185`                                                 |
| `--ayr-connect-status-radius`      | `4px`                                                     |
| `--ayr-connect-status-success-bg`  | `#EBFFF8`                                                 |
| `--ayr-connect-status-success-fg`  | `#237C5C`                                                 |
| `--ayr-connect-status-warning-bg`  | `#FFF7EF`                                                 |
| `--ayr-connect-status-warning-fg`  | `#702E00`                                                 |
| `--ayr-connect-status-critical-bg` | `#FFE5E1`                                                 |
| `--ayr-connect-status-critical-fg` | `#AD1902`                                                 |
| `--ayr-connect-status-info-bg`     | `#ECF9FF`                                                 |
| `--ayr-connect-status-info-fg`     | `#1F6686`                                                 |
| `--ayr-connect-callout-bg`         | `#FFF7EF`                                                 |
| `--ayr-connect-callout-fg`         | `#702E00`                                                 |
| `--ayr-connect-partner-name`       | `""`                                                      |
| `--ayr-connect-icon-size`          | `32px`                                                    |
| `--ayr-connect-avatar-size`        | `40px`                                                    |

A value that is not valid CSS for its token is **ignored, with a warning in your console**, rather
than applied. That matters more than it sounds: a unitless `8` for `--ayr-connect-spacing` is a
perfectly innocent string that would invalidate every calculation reading it and collapse the
layout, with no error anywhere. Give lengths a unit.

### Your Own Name on the Hand-Off Screen

Before we hand your user to a network, we show a short screen naming who they are connecting
through. Two hooks let you make that your own, and both are version-stable.

`--ayr-connect-partner-name` sets the label. It is the one token whose value is **text**, so it has
to be quoted as a CSS string — an unquoted value is invalid and renders nothing at all:

```javascript theme={"system"}
AyrshareConnect.init({
  session: getSession,
  appearance: { "--ayr-connect-partner-name": "'Acme Social'" },
  css: "[data-ayr-connect-partner-mark]::after { background-image: url('https://cdn.example.com/mark.svg') }",
});
```

The logo is not a token. We ship the mark as a positioned, sized element and you fill it with a
`background-image` through `css`, as above — a token that could fetch an image from inside our
document is not something we accept, so the request comes from a rule you wrote rather than from a
value you passed us.

<Note>
  `[data-ayr-connect-partner-mark]` and `[data-ayr-connect-partner-name]` are the **exception** to
  the custom-CSS caveat below: these two selectors are part of the contract and we keep them across
  versions.
</Note>

### Custom CSS

`css` takes a string applied inside every frame, for the cases tokens do not cover.

```javascript theme={"system"}
AyrshareConnect.init({ session: getSession, css: "button { letter-spacing: 0.01em }" });
```

<Warning>
  **Custom CSS is not supported across versions.** Its selectors target our internal markup, which
  changes between releases — a rule that works today can silently stop matching after any update.
  The token contract above is the part we keep. Pin a version if you depend on custom CSS.
</Warning>

Popups inherit the instance's `appearance` and `css` exactly as frames do, so your user does not
watch our neutral defaults appear halfway through a flow.

## Worth Knowing Before You Ship

<AccordionGroup>
  <Accordion title="A popup whose session has expired reports cancelled, not error">
    A popup you opened with `popup()` cannot be told why a token was refused — to say so it would
    have to trust an origin it has not yet validated, which our security model does not allow. So a
    popup carrying a dead token closes and surfaces as `cancelled` with `reason: "popupClosed"`
    rather than `error`.

    In practice this is rare: a popup opened before a silent refresh keeps working, because it
    validated its token when it opened. If you see unexplained `popupClosed` results, check that
    your `session` endpoint is returning a fresh session.
  </Accordion>

  <Accordion title="One session per instance, not one per mount">
    Fourteen mounts share one token and cost one call to your backend, not fourteen. If you want
    differently scoped slots — a different `allowedSocial` on some of them — run a second `init`
    with its own session rather than expecting a mount to narrow it.
  </Accordion>

  <Accordion title="The frames only render on the origin you declared">
    Every session carries the `origin` your page runs on, and a frame compares the page embedding it
    against that value before rendering anything. A frame embedded somewhere else stays blank and
    sends no events. There is no allowlist to register and nothing to configure — send the right
    `origin` when you create the session.
  </Accordion>

  <Accordion title="Height is handled for you, and is not an event">
    Frames report their height to the script, and the script resizes them. Your layout just reflows.
    There is no resize event to subscribe to, and nothing to measure on your side.
  </Accordion>
</AccordionGroup>

## Requirements

<ul className="custom-bullets">
  <li>
    The **[Max Pack](/docs/additional/maxpack)**. A widget session is a connect-mode session, and
    creating one without the Max Pack returns `code: 504`. Contact support if you need connect mode
    enabled on an account without it.
  </li>

  <li>
    An **`origin`** on every session — the exact origin your page runs on. Omitting it returns
    `code: 505`; a value that is not an `https` origin, a custom scheme, or `http://localhost`
    returns `code: 506`.
  </li>

  <li>
    **No `network`** on the session. That parameter is what makes a session
    [direct mode](/docs/multiple-users/connect-direct-mode) instead, and a direct-mode session's URL is
    not what the script expects.
  </li>
</ul>

Every code above is in the
[Link Session Errors](/docs/errors/errors-ayrshare#link-session-errors) reference, with the exact message
the API returns and what to do about it.
