> This page is for version 2025-09-18 (default).
> For other versions, use one of these documentation indexes:
> - 2025-09-18 (default): https://apidocs.polytomic.com/2025-09-18/llms.txt
> - 2024-02-08: https://apidocs.polytomic.com/2024-02-08/llms.txt
> - 2023-04-25: https://apidocs.polytomic.com/2023-04-25/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://apidocs.polytomic.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://apidocs.polytomic.com/_mcp/server.

`@polytomic/connect-react` renders the configuration form inside your React app,
so users connect their systems without leaving your product. For non-React
frontends, or to let Polytomic host the flow, see
[Hosted authentication](/guides/connect/hosted).

## Install

```bash
npm install @polytomic/connect-react
```

Requires `react` and `react-dom` 18 or 19.

## Add two backend routes

Set up a proxy on your backend to mint Connect sessions and forward requests to
Polytomic.

**`server.ts`**

```ts title="server.ts"
import express from "express";

const app = express();
app.use(express.json());

// Returns a token for one connection, as plain text.
app.post("/connect-session", async (req, res) => {
  // Decide these from your own records, not from the request.
  const { name, type } = await connectionFor(req);

  const response = await fetch(
    "https://app.polytomic.com/api/connections/connect/",
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.POLYTOMIC_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ name, whitelist: [type] }),
    },
  );
  const body = await response.json();
  if (!response.ok) {
    res.status(response.status).send(body.message ?? body.error?.message);
    return;
  }
  res.type("text/plain").send(body.data.token);
});

// Forwards the form's calls to the Polytomic API.
app.use("/polytomic", async (req, res) => {
  const response = await fetch(`https://app.polytomic.com/api${req.url}`, {
    method: req.method,
    headers: {
      "Content-Type": "application/json",
      "X-Polytomic-Version": req.get("X-Polytomic-Version") ?? "2025-09-18",
    },
    body: req.method === "GET" ? undefined : JSON.stringify(req.body),
  });
  res
    .status(response.status)
    .type("json")
    .send(await response.text());
});
```

The embedded form shows one connection type, so send exactly one entry in
`whitelist`. List the available types with
[`GET /api/connection_types`](/api-reference/connections/get-types), and send a
type's `id`. Each type also has a `logo_url` and `logo_dark_url` for rendering
your own picker. `name` is how the connection will appear in the organization,
and must be unique per connection type. To reconnect an existing connection,
send `connection` with its ID instead of `whitelist`. The other fields are in
the [API reference](/api-reference/connections/connect).

## Render the form

```tsx
import { ConnectionForm } from "@polytomic/connect-react";
import "@polytomic/connect-react/styles.css";

async function getSession() {
  const response = await fetch("/connect-session", { method: "POST" });
  const text = await response.text();
  if (!response.ok) throw new Error(text);
  return text;
}

<ConnectionForm
  apiBaseUrl="/polytomic"
  getSession={getSession}
  onComplete={(connection) => save(connection.id)}
  onCancel={() => closeDialog()}
/>;
```

| Prop          | Purpose                                                                                                    |
| ------------- | ---------------------------------------------------------------------------------------------------------- |
| `getSession`  | Required. Returns a session token from your backend. Called once on mount.                                 |
| `apiBaseUrl`  | Path of your proxy route. Default `/api`.                                                                  |
| `onComplete`  | Receives the connection's `id`, `name`, `type` and `status` once connected.                                |
| `onCancel`    | Called on **Cancel**. Close or unmount the form here.                                                      |
| `onError`     | Called when the session can't load or the flow can't continue.                                             |
| `colorScheme` | `'light'` (default) or `'dark'`, to match the surface behind the form.                                     |
| `layout`      | `'contained'` (default) fills a card or dialog. `'page'` keeps fields at a standard width on an open page. |

Validation errors, such as a missing field or failed credentials, display in the
form and don't call `onError`.

OAuth opens in a popup. If the browser blocks it, the form shows a button to
open it.

If the user leaves before finishing, an incomplete connection can remain in the
organization. Check a connection's `status` before using it, and delete unused
ones.

To restart the flow, remount the form with a new `key`.

## Style it

Import the package stylesheet before your own CSS so your rules take precedence.
Polytomic styles are scoped to the form's root element, `.ptc-root`.

### Theme

Theme elements like color and border radius are styled using CSS variables.
Override them on `.ptc-root`, and set dark values under
`[data-color-scheme='dark']`. Colors are HSL channels without the `hsl()`
wrapper:

```css
.ptc-root {
  --ring: 250 60% 50%;
  --radius: 0.25rem;
}
.ptc-root[data-color-scheme="dark"] {
  --ring: 250 70% 70%;
}
```

| Tokens                                                                                     | Used for                                      |
| ------------------------------------------------------------------------------------------ | --------------------------------------------- |
| `--foreground`, `--muted-foreground`                                                       | Text, and secondary text such as descriptions |
| `--background`, `--card`, `--popover`, `--popover-foreground`, `--muted`                   | Surfaces: cards, menus, read-only inputs      |
| `--border`, `--border-strong`, `--input`, `--ring`                                         | Input outlines, input fill and the focus ring |
| `--button`, `--button-foreground`, `--button-hover`, `--button-active`, `--button-border`  | Buttons                                       |
| `--control`, `--control-foreground`, `--control-hover`, `--control-active`                 | Chips and small controls                      |
| `--secondary`, `--secondary-hover`, `--accent`, `--accent-foreground`                      | Hover and highlighted options                 |
| `--selected`, `--selected-foreground`, `--selected-strong`, `--selected-strong-foreground` | Chosen options and days                       |
| `--primary`                                                                                | The Cancel link                               |
| `--destructive`                                                                            | Errors                                        |
| `--radius`                                                                                 | Menus and the calendar                        |

Set `font-family` on `.ptc-root` to change the font, which defaults to Inter.
Set `--ptc-form-max-width` to change the maximum width, which defaults to
`36rem`.

### Elements

Elements are styled by `data-slot`, with state exposed through data and ARIA
attributes. Scope rules under `.ptc-root` and include the state you're
targeting:

```css
.ptc-root [data-slot="input"][aria-invalid="true"] {
  box-shadow: 0 0 0 2px crimson;
}
.ptc-root [data-slot="combobox"][data-state="open"] {
  box-shadow: 0 0 0 2px var(--acme-brand);
}
```

Only `data-slot` values and state attributes are stable. Other markup can change
between versions.

| Element           | `data-slot` values                                                                                                         |
| ----------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Field             | `field`, `field-label`, `field-control`, `field-description`, `field-error`                                                |
| Text input        | `input`, `input-wrapper`, `input-copy-button`, `textarea`                                                                  |
| Checkbox          | `checkbox`, `checkbox-label`                                                                                               |
| Select            | `combobox`, `combobox-menu`, `combobox-search`, `combobox-option`, `combobox-clear`                                        |
| Date picker       | `date-picker`, `date-picker-wrapper`, `date-picker-calendar`, `calendar-day`                                               |
| Key-value list    | `key-value`, `key-value-row`, `key-value-add-button`, `key-value-remove-button`                                            |
| Hierarchy select  | `hierarchy-select`, `hierarchy-select-tree`, `hierarchy-select-row`, `hierarchy-select-refresh`, `hierarchy-select-footer` |
| File upload       | `file-upload`, `file-upload-dropzone`, `file-upload-file-name`, `file-upload-button`                                       |
| Google Drive file | `drive-file`, `drive-file-browse-button`, `drive-file-list`, `drive-file-file-row`, `drive-file-refresh`                   |
| HTTP auth         | `http-auth`                                                                                                                |
| Actions           | `button`, `button-wrapper`, `link`, `form-actions`, `form-actions-cancel`                                                  |
| Flow              | `form-container`, `form`, `form-status`, `form-error`                                                                      |

Common states:

* `data-invalid`, `data-disabled` and `data-required` on a field
* `aria-invalid` on an input
* `data-state="open"` on menus
* `data-checked` on a selected menu option
* `data-selected` on selected tree and file rows

Tooltips and dialogs are portalled to a separate `.ptc-root` container appended
to the body, so rules scoped to `.ptc-root` apply to them too.