---
title: Building Web Components
description: Extend the platform with your own widgets — develop in your framework, bundle, upload, and register a third-party web component
date: 2026-07-22
order: 1
tags:
  - web-components
  - widgets
  - extensibility
---

You can extend the platform with your own widgets: build a **web component** in the
framework you already know, bundle it with the provided toolchain, upload it, and
register it in your project's web component registry. It then appears in the
[Widget Catalog](/widget-framework/widget-catalog) like any built-in widget — running
inside a security sandbox, styled with the platform theme, and gated by an end-user
[consent overlay](./consent.md).

## The journey at a glance

1. **Clone** the starter repository
2. **Pick** a framework sample (Svelte, React, Vue, Angular, or Solid)
3. **Develop** against the SDK with the local dev harness
4. **Bundle** to a single ESM file
5. **Upload & register** in the backoffice
6. **Place** the component from the widget catalog

## 1. Clone the starter repository

```bash
git clone https://github.com/smallstack/business-platform-web-components.git
cd business-platform-web-components
npm install
npm run build
```

The repository contains the bridge SDK (`packages/sdk`), a local dev harness
(`packages/dev-harness`), the shared bundling toolchain (`packages/build-config`),
and one complete sample per framework under `samples/`.

## 2. Pick a framework

Copy the sample closest to your stack — each demonstrates the full feature set
(data subscription, theming, resize, graceful denied-permission states):

| Sample | Pattern |
| --- | --- |
| `samples/svelte` | Reference template — Svelte custom element (`<svelte:options customElement>`) |
| `samples/react` | Hand-written element wrapping a React root |
| `samples/vue` | `defineCustomElement` — shadow-DOM reference sample |
| `samples/angular` | `@angular/elements`, zoneless standalone component |
| `samples/solid` | Hand-written element wrapping a Solid `render` |
| `samples/vanilla` | Minimal framework-free element |

Your component must register a **custom element** as a side effect of being imported
(`customElements.define("my-company-widget", …)`), with a lower-case, dash-separated
tag name.

## 3. Develop with the SDK and dev harness

All platform data arrives through `@smallstack/web-component-sdk` — never talk to the
bridge with raw `postMessage`:

```ts
import { connect } from "@smallstack/web-component-sdk";

const client = connect(hostElement);
client.adoptThemeStylesheets(shadowRoot); // platform theme, live light/dark
client.observeResize(); // the host iframe sizes itself

const { grantedScopes } = await client.whenInitialized();
const profile = client.getUserProfile(); // user-profile:read
client.onData(() => {
  const orders = client.getCollection("orders"); // collection:orders:read
});
```

The dev harness plays the platform host locally, with production-identical sandbox
isolation, mock data resolved deny-by-default from your declared permissions, a
light/dark toggle, and hot reload:

```bash
npm run dev   # in your sample workspace
```

Mock permissions and fixtures live in the workspace's `harness.config.json` — see the
starter repo's `packages/dev-harness` README for the full reference.

## 4. Bundle

```bash
npm run build   # emits dist/bundle.mjs
```

The shared toolchain produces exactly the format the platform serves: **one ESM
file**, no externals or code-splitting, styles inlined (shadow DOM), maximum 10 MB.
The bundle's SHA-256 is pinned when you upload, so builds are deterministic.

## 5. Upload & register in the backoffice

In your project's settings, open **Web Components** and create a registration:

- **Title** and **category** — how the component appears in the widget catalog
- **Element name** — must match your `customElements.define` tag exactly
- **Permissions** — the [declared scopes](./permissions.md) your component requests

Then upload `dist/bundle.mjs`. Every upload creates a new immutable version; the
registration must have a bundle before it can be set to **active**. Only active
registrations appear in the catalog — **disabled** acts as a kill switch that stops
already-placed components from loading.

## 6. Place the component

Editors find the registration in the [Widget Catalog](/widget-framework/widget-catalog)
under its category and place it on pages or dashboards. If the component declares
collection scopes, the editor binds them to concrete datatypes at placement time.
Before your code runs for any end user, the platform shows the
[consent overlay](./consent.md).

## Related

- [Permissions](./permissions.md) — what data your component can request
- [Security model](./security.md) — what the sandbox allows and forbids
- [Consent behavior](./consent.md) — when end users are prompted
- [Theming](./theming.md) — platform tokens and light/dark switching
