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

# StackOne Hub Web Component

> Embed StackOne Hub in any framework using the framework-agnostic <stackone-hub> custom element

`@stackone/hub` ships a framework-agnostic custom element, `<stackone-hub>`, that you can drop into any HTML page or framework template. React and ReactDOM are bundled into the web-component build, so consumers don't need React installed.

Use the web component for any non-React stack, or when you want a single code path across multiple frameworks. For a native React app, the [StackOne Hub (Native)](/embed/account-linking/stackone-hub) is usually a better fit — it integrates with your React tree and lets you pass callbacks as props.

## Quick start

<Steps>
  <Step title="Install and register">
    Install the package and register `<stackone-hub>`. The import is side-effecting — it registers `<stackone-hub>` on `customElements`.

    <Tabs>
      <Tab title="Vanilla HTML">
        Point a `<script>` tag at the IIFE bundle on a CDN. The script is side-effecting — it registers `<stackone-hub>` on `customElements`.

        ```html theme={null}
        <script src="https://unpkg.com/@stackone/hub/dist/webcomponent.js"></script>
        ```

        If you'd rather install via npm and bundle the script yourself, use the `import '@stackone/hub/webcomponent'` pattern shown in the other tabs.
      </Tab>

      <Tab title="React">
        <Note>
          If you're starting fresh in React, the [`<StackOneHub>` component](/embed/account-linking/stackone-hub) is usually a better fit — it integrates with your React tree and lets you pass callbacks as props. Reach for the web component when you can't add another copy of React to your bundle, or when you want the same code path as your non-React surfaces.
        </Note>

        Install the package:

        <CodeGroup>
          ```bash npm theme={null}
          npm install @stackone/hub
          ```

          ```bash yarn theme={null}
          yarn add @stackone/hub
          ```

          ```bash pnpm theme={null}
          pnpm add @stackone/hub
          ```
        </CodeGroup>

        Register the element at app entry:

        ```tsx theme={null}
        // src/main.tsx
        import '@stackone/hub/webcomponent';
        import { StrictMode } from 'react';
        import { createRoot } from 'react-dom/client';
        import App from './App';

        const root = document.getElementById('root');
        if (!root) {
          throw new Error('#root element not found');
        }
        createRoot(root).render(
          <StrictMode>
            <App />
          </StrictMode>,
        );
        ```

        Tell TypeScript that `<stackone-hub>` is a valid JSX tag:

        ```tsx theme={null}
        declare global {
          // biome-ignore lint/style/noNamespace: JSX intrinsic-element augmentation requires namespace syntax.
          namespace JSX {
            interface IntrinsicElements {
              'stackone-hub': React.DetailedHTMLProps<
                React.HTMLAttributes<HTMLElement> & {
                  token?: string;
                  mode?: string;
                  'base-url'?: string;
                  'app-url'?: string;
                  'account-id'?: string;
                  height?: string;
                  theme?: string;
                  'show-footer-links'?: boolean | string;
                  'on-close-label'?: string;
                },
                HTMLElement
              >;
            }
          }
        }
        ```
      </Tab>

      <Tab title="Vue 3">
        Install the package:

        <CodeGroup>
          ```bash npm theme={null}
          npm install @stackone/hub
          ```

          ```bash yarn theme={null}
          yarn add @stackone/hub
          ```

          ```bash pnpm theme={null}
          pnpm add @stackone/hub
          ```
        </CodeGroup>

        Vue's template compiler treats unknown tags as Vue components by default. Mark `stackone-hub` as a custom element so it falls through to the DOM:

        ```ts theme={null}
        // vite.config.ts
        import vue from '@vitejs/plugin-vue';
        import { defineConfig } from 'vite';

        export default defineConfig({
          plugins: [
            vue({
              template: {
                compilerOptions: {
                  isCustomElement: (tag) => tag === 'stackone-hub',
                },
              },
            }),
          ],
        });
        ```

        Register the element at app entry:

        ```ts theme={null}
        // src/main.ts
        import '@stackone/hub/webcomponent';
        import { createApp } from 'vue';
        import App from './App.vue';

        createApp(App).mount('#app');
        ```
      </Tab>

      <Tab title="Svelte 5">
        Svelte treats unknown tags as custom elements automatically — no extra config required.

        Install the package:

        <CodeGroup>
          ```bash npm theme={null}
          npm install @stackone/hub
          ```

          ```bash yarn theme={null}
          yarn add @stackone/hub
          ```

          ```bash pnpm theme={null}
          pnpm add @stackone/hub
          ```
        </CodeGroup>

        Register the element at app entry:

        ```ts theme={null}
        // src/main.ts
        import '@stackone/hub/webcomponent';
        import { mount } from 'svelte';
        import App from './App.svelte';

        const target = document.getElementById('app');
        if (!target) {
          throw new Error('#app element not found');
        }
        mount(App, { target });
        ```
      </Tab>

      <Tab title="Angular">
        Install the package:

        <CodeGroup>
          ```bash npm theme={null}
          npm install @stackone/hub
          ```

          ```bash yarn theme={null}
          yarn add @stackone/hub
          ```

          ```bash pnpm theme={null}
          pnpm add @stackone/hub
          ```
        </CodeGroup>

        Add `CUSTOM_ELEMENTS_SCHEMA` to the component (or module) that uses `<stackone-hub>` so Angular accepts unknown tags as custom elements:

        ```ts theme={null}
        // src/app/app.component.ts
        import { CUSTOM_ELEMENTS_SCHEMA, Component } from '@angular/core';

        @Component({
          selector: 'app-root',
          standalone: true,
          schemas: [CUSTOM_ELEMENTS_SCHEMA],
          templateUrl: './app.component.html',
        })
        export class AppComponent {}
        ```

        Register the element at app entry:

        ```ts theme={null}
        // src/main.ts
        import '@stackone/hub/webcomponent';
        import { bootstrapApplication } from '@angular/platform-browser';
        import { AppComponent } from './app/app.component';

        bootstrapApplication(AppComponent).catch((err) => console.error(err));
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Get a session token from your backend">
    The Hub needs a [connect session token](/embed/connect-session) to communicate with StackOne. Generate it server-side to keep your API key off the client.

    ```typescript theme={null}
    async function retrieveConnectSessionToken() {
      const response = await fetch('/api/stackone/connect-session', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ username: 'jane@example.com' }),
      });
      const { token } = await response.json();
      return token;
    }
    ```

    To route the session through a specific **connector profile** (when a provider has more than one), pass its `connector_profile_id` when creating the session — see [Target a specific connector profile](/embed/connect-session#target-a-specific-connector-profile).
  </Step>

  <Step title="Mount and wire the Hub">
    Mount the element, set the token, and listen for events:

    <Tabs>
      <Tab title="Vanilla HTML">
        ```html theme={null}
        <stackone-hub id="hub" mode="integration-picker" height="600px"></stackone-hub>

        <script type="module">
          const hub = document.getElementById('hub');
          const token = await retrieveConnectSessionToken();
          hub.setAttribute('token', token);

          hub.addEventListener('success', (event) => {
            console.log('Connected:', event.detail.id, event.detail.provider);
          });

          hub.addEventListener('close', () => {
            console.log('Closed');
          });
        </script>
        ```
      </Tab>

      <Tab title="React">
        React 19 passes unknown props through as attributes, but events still need to be wired with `addEventListener` via a `ref` — React's `onSuccess={...}` JSX shorthand does **not** fire on native DOM `CustomEvent`s.

        ```tsx theme={null}
        import { useEffect, useRef, useState } from 'react';

        export default function App() {
          const [token, setToken] = useState('');
          const hubRef = useRef<HTMLElement>(null);

          useEffect(() => {
            retrieveConnectSessionToken().then(setToken);
          }, []);

          useEffect(() => {
            const el = hubRef.current;
            if (!el) return;
            const onSuccess = (event: Event) => {
              const detail = (event as CustomEvent).detail;
              console.log('Connected:', detail.id, detail.provider);
            };
            const onClose = () => console.log('Closed');
            el.addEventListener('success', onSuccess);
            el.addEventListener('close', onClose);
            return () => {
              el.removeEventListener('success', onSuccess);
              el.removeEventListener('close', onClose);
            };
          }, []);

          return (
            <stackone-hub
              ref={hubRef}
              token={token || undefined}
              mode="integration-picker"
              height="600px"
            />
          );
        }
        ```
      </Tab>

      <Tab title="Vue 3">
        Vue passes `:prop="value"` bindings through as HTML attributes when the tag is registered as a custom element. Vue's `@event` shorthand does **not** fire on native DOM `CustomEvent`s — subscribe with `addEventListener` via a `ref` instead.

        ```vue theme={null}
        <script setup lang="ts">
        import { onBeforeUnmount, onMounted, ref, useTemplateRef } from 'vue';

        const token = ref('');
        const hubRef = useTemplateRef<HTMLElement>('hub');

        const onSuccess = (event: Event) => {
          const detail = (event as CustomEvent).detail;
          console.log('Connected:', detail.id, detail.provider);
        };
        const onClose = () => console.log('Closed');

        onMounted(async () => {
          token.value = await retrieveConnectSessionToken();
          hubRef.value?.addEventListener('success', onSuccess);
          hubRef.value?.addEventListener('close', onClose);
        });

        onBeforeUnmount(() => {
          hubRef.value?.removeEventListener('success', onSuccess);
          hubRef.value?.removeEventListener('close', onClose);
        });
        </script>

        <template>
          <stackone-hub
            ref="hub"
            :token="token"
            mode="integration-picker"
            height="600px"
          ></stackone-hub>
        </template>
        ```
      </Tab>

      <Tab title="Svelte 5">
        Svelte passes attribute bindings through directly. Its `on:event` shorthand **does** fire on native DOM `CustomEvent`s, so `<stackone-hub on:success={...} on:close={...}>` works. The example below uses `addEventListener` via `bind:this` to keep the typing explicit.

        ```svelte theme={null}
        <script lang="ts">
            let token = $state('');
            let hubEl: HTMLElement | undefined = $state();

            $effect(() => {
                retrieveConnectSessionToken().then((t) => {
                    token = t;
                });
            });

            $effect(() => {
                if (!hubEl) return;
                const onSuccess = (event: Event) => {
                    const detail = (event as CustomEvent).detail;
                    console.log('Connected:', detail.id, detail.provider);
                };
                const onClose = () => console.log('Closed');
                hubEl.addEventListener('success', onSuccess);
                hubEl.addEventListener('close', onClose);
                return () => {
                    hubEl?.removeEventListener('success', onSuccess);
                    hubEl?.removeEventListener('close', onClose);
                };
            });
        </script>

        <stackone-hub
            bind:this={hubEl}
            {token}
            mode="integration-picker"
            height="600px"
        ></stackone-hub>
        ```
      </Tab>

      <Tab title="Angular">
        Bind attributes with `[attr.name]="value"` to keep them as attributes rather than Angular property bindings. Angular's `(event)` template binding **does** fire on native DOM `CustomEvent`s, so `(success)="onSuccess($event)"` works. The example below uses `addEventListener` via `@ViewChild` for explicit teardown in `ngOnDestroy`.

        ```ts theme={null}
        // src/app/app.component.ts
        import {
          AfterViewInit,
          Component,
          CUSTOM_ELEMENTS_SCHEMA,
          ElementRef,
          OnDestroy,
          ViewChild,
        } from '@angular/core';

        @Component({
          selector: 'app-root',
          standalone: true,
          schemas: [CUSTOM_ELEMENTS_SCHEMA],
          template: `
            <stackone-hub
              #hub
              [attr.token]="token || null"
              mode="integration-picker"
              height="600px"
            ></stackone-hub>
          `,
        })
        export class AppComponent implements AfterViewInit, OnDestroy {
          @ViewChild('hub') hubRef?: ElementRef<HTMLElement>;
          token = '';

          private onSuccess = (event: Event) => {
            const detail = (event as CustomEvent).detail;
            console.log('Connected:', detail.id, detail.provider);
          };
          private onClose = () => console.log('Closed');

          async ngAfterViewInit() {
            this.token = await retrieveConnectSessionToken();
            this.hubRef?.nativeElement.addEventListener('success', this.onSuccess);
            this.hubRef?.nativeElement.addEventListener('close', this.onClose);
          }

          ngOnDestroy() {
            this.hubRef?.nativeElement.removeEventListener('success', this.onSuccess);
            this.hubRef?.nativeElement.removeEventListener('close', this.onClose);
          }
        }
        ```
      </Tab>
    </Tabs>
  </Step>

  <Step title="Let users link their accounts">
    Your end-users now use the Hub to connect their accounts. Each successful link fires the `success` event with the linked account id — persist against your user — it's the account ID you'll use with every [action call](/embed/call-actions/overview).

    You can also capture it server-side from the `account.created` [webhook event](/embed/handle-account-events).
  </Step>
</Steps>

## Attributes

The `<stackone-hub>` element accepts these attributes — all scalar `StackOneHub` properties map to kebab-case HTML attributes:

| Attribute           | Type                             | Default                    | Description                                             |
| ------------------- | -------------------------------- | -------------------------- | ------------------------------------------------------- |
| `token`             | string                           | —                          | Connect session token from your backend                 |
| `mode`              | `integration-picker`             | `integration-picker`       | Hub mode                                                |
| `base-url`          | string                           | `https://api.stackone.com` | StackOne API base URL                                   |
| `app-url`           | string                           | `https://app.stackone.com` | StackOne App URL                                        |
| `height`            | string                           | `500px`                    | Component height                                        |
| `theme`             | `light` \| `dark` \| JSON object | `light`                    | Theme keyword or a JSON-encoded `PartialMalachiteTheme` |
| `account-id`        | string                           | —                          | Pre-select a specific account to edit                   |
| `on-close-label`    | string                           | `Close`                    | Custom label for the close button                       |
| `show-footer-links` | boolean attribute                | `true`                     | Toggle footer visibility                                |

## Events

Callbacks are dispatched as native DOM `CustomEvent`s on the host element with `bubbles: true` and `composed: true`.

| Event     | `event.detail`                     |
| --------- | ---------------------------------- |
| `success` | `{ id: string; provider: string }` |
| `close`   | `undefined`                        |

<Note>
  Framework-emitted event shorthands behave differently from native DOM `CustomEvent`s. React's `onSuccess={...}` and Vue's `@event` do **not** fire on them — subscribe with `addEventListener` via a ref. Svelte's `on:event` and Angular's `(event)` **do** fire on them.
</Note>

## Next steps

<CardGroup cols={2}>
  <Card title="Call Actions" icon="code" href="/embed/call-actions/overview">
    With the account linked, call its actions from your product — over the Agent SDK, MCP, A2A, or RPC/HTTP.
  </Card>

  <Card title="Embed Overview" icon="map" href="/embed/getting-started">
    Return to the end-to-end embedding journey.
  </Card>
</CardGroup>
