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

# Pairing Card

> The pairing UI Crosslink ships, and the little you configure

Crosslink ships the desktop pairing experience. You mount it; you do not build it.

```js theme={null}
import { createPairingCard } from "@crosslink/sdk-browser";

createPairingCard({ target: "#crosslink" });
```

That is a complete integration. The card renders the QR, the nine-digit code and
the countdown, mints a fresh session when the current one is close to expiring,
mints another when a device redeems one, exposes the connection-mode settings,
lists paired devices and revokes them, and shows the Crosslink mark.

It also takes your application's name, icon and colours from the host: the
`application` block you pass to `createCrosslinkServer` travels with each
pairing session, so the desktop page states none of it a second time.

With no `source` option, the card uses the Crosslink system endpoints your host
already exposes (`/__crosslink/*`). You do not write those routes. The old
`source: true` spelling remains a compatibility alias, but omission is the
canonical path.

<Note>
  Migration for older controlled integrations: omission now means the canonical
  HTTP source. Add `source: false` before continuing to drive the card with
  `update()`. Existing `source: true`, custom base-path strings, and custom
  `PairingSource` objects keep their behavior.
</Note>

## Where the branding comes from

Configure it once, on the host:

```ts theme={null}
createCrosslinkServer({
  application: {
    id: "com.example.chat",
    name: "Example Chat",
    icon: "/icon-192.png",
    accentColor: "#38bdf8",
    backgroundColor: "#0f172a",
    appearance: "dark"
  },
  mobile: { entry: "./mobile.html" }
});
```

The same block drives the pairing card, the installable manifest, the mobile
onboarding, the offline screen and the revoked screen. Nothing is repeated in
the page.

## Overriding it in the page

Only when the pairing screen should differ from your host metadata. Options win
over what the host reports, field by field — set the name and the colours still
come from the host.

```js theme={null}
createPairingCard({
  target: "#crosslink",

  appName: "Pair with the desk",
  appIcon: "/pairing-icon.png",
  brand: { accentColor: "#f97316" },
  blurb: "<strong>Connect another device</strong> to use Chat from your phone."
});
```

| Option                  | What it does                                                     |
| ----------------------- | ---------------------------------------------------------------- |
| `appName`               | Shown beside the Crosslink mark.                                 |
| `appIcon`               | Your icon, next to your name. Never replaces the Crosslink mark. |
| `brand.accentColor`     | Actions, and the tint applied to the Crosslink mark.             |
| `brand.backgroundColor` | Card background. Decides light/dark when `appearance` is unset.  |
| `brand.textColor`       | Primary text. Derived from the background when omitted.          |
| `brand.appearance`      | `"light"`, `"dark"`, or derived from the background.             |
| `blurb`                 | One sentence in your own words about what pairing does.          |

There is no option that replaces the card's markup, swaps its renderer, or
removes the Crosslink mark. That is deliberate: the value of a recognisable
pairing screen is that it is the same screen in every Crosslink application.

The card carries the wordmark and no attribution footer, divider, link or
reserved footer space. The Crosslink attribution footer belongs to the
authorized mobile app shell — see
[Mobile bootstrap](/client/mobile-bootstrap#the-crosslink-attribution-footer).

## While a setting is applied

Changing the connection mode is a round trip to the host: it re-derives which
routes to advertise and may ask your router for a port mapping, which routinely
takes seconds. The settings popover shows a spinner and refuses further changes
until a session minted under the new mode is on screen, so a slow mode change
never looks like a click that did nothing.

## Framework bindings

The card is plain DOM, so it works anywhere. React gets a thin wrapper that
mounts the same implementation:

```tsx theme={null}
import { CrosslinkPairingCard } from "@crosslink/react";

<CrosslinkPairingCard
  appName="Crosslink Chat"
  brand={{ accentColor: "#38bdf8" }}
/>;
```

Vue, Svelte and plain pages call `createPairingCard` directly. There is no
framework-specific reimplementation to keep in step.

## Reacting to pairing

The card drives itself, so callbacks are for your UI, not for its behaviour.

```js theme={null}
createPairingCard({
  target: "#crosslink",
  onSession: (session) => console.log("code", session.code),
  onDeviceConnected: (deviceId) => showDashboard(deviceId),
  onError: (error) => console.warn("pairing unavailable:", error.message)
});
```

`onError` fires *after* the card has already rendered the failure and scheduled
a retry. You do not need to build recovery on top of it.

## When your host is not reachable over HTTP

An Electron renderer behind a preload bridge, or any page that cannot call the
host directly, supplies its own source:

```js theme={null}
createPairingCard({
  target: "#crosslink",
  source: {
    getSession: (mode) => window.api.getPairingSession(mode),
    setNetworkMode: (mode) => window.api.setNetworkMode(mode),
    subscribe: (listener) => window.api.onCrosslinkEvent(listener)
  }
});
```

The source decides where a pairing session comes from. It never decides what the
pairing screen looks like — both paths render the identical card.

## Controlled mode (advanced)

Set `source: false` and the card renders only what you pass to `update()`.
This explicit opt-out exists for hosts that already own a pairing loop; it is
not the normal integration path, and choosing it means writing the loop the
card would otherwise run for you.

```js theme={null}
const card = createPairingCard({ target: "#crosslink", source: false });
card.update({ code: "123456789", qr: svg, expiresAt: Date.now() + 120_000 });
```
