# Custom components

> Build custom components for PandaSuite using the PandaBridge libraries (JavaScript & React). Define properties, events, actions, markers, synchronization, and more.

https://docs.pandasuite.com/essentials/components/custom-components/

Custom components let you extend PandaSuite with your own interactive elements built using web technologies (HTML, CSS, JavaScript). Unlike native PandaSuite components, custom components run in a web view, giving you access to the entire web ecosystem: npm packages, React, Vue, or any JavaScript framework.

Using the PandaBridge libraries, your component can communicate with the rest of your PandaSuite project: receiving properties, triggering events, responding to actions, and synchronizing with other components.

> **Info**
>
> If you only need to embed external web content without interacting with other PandaSuite components, use the [Web component](https://docs.pandasuite.com/essentials/components/web/) instead.

## Use cases

Custom components are useful when you need to:

- **Integrate third-party services**: connect to APIs like payment processors, analytics platforms, or custom backends
- **Add specialized functionality**: build QR code scanners, signature pads, custom charts, or interactive visualizations
- **Reuse existing code**: package your JavaScript or React code as a PandaSuite component
- **Extend native capabilities**: access device features or browser APIs not available in standard components

## What custom components can do

Custom components behave like native PandaSuite components. They can:

- Display configurable **properties** in the Studio property panel
- Emit **events** (triggers) that activate actions on other components
- Receive **actions** from other components
- Define **markers** (data points like timestamps or hotspots)
- **Synchronize** state with other components
- React to **language changes** in multilingual projects
- Receive **dynamic data** through data binding

## Component manifest (pandasuite.json)

Every custom component includes a `pandasuite.json` file that describes its interface. This manifest defines what properties, events, actions, and other features appear in PandaSuite Studio.

### Basic structure

```json
{
  "version": "2.0.0",
  "main": "",
  "properties": [],
  "events": [],
  "actions": [],
  "resources": [],
  "markers": [],
  "synchronization": [],
  "queryable": {},
  "system": {}
}
```

| Field             | Description                                                     |
| ----------------- | --------------------------------------------------------------- |
| `version`         | Manifest version (current: `"2.0.0"`)                           |
| `main`            | Entry point URL (usually empty `""` for PandaBridge components) |
| `properties`      | Configuration options shown in the property panel               |
| `events`          | Triggers that can activate actions on other components          |
| `actions`         | Operations other components can call                            |
| `resources`       | Media slots (images, videos, audio, zip files)                  |
| `markers`         | User-defined data points                                        |
| `synchronization` | Groups for cross-component state sync                           |
| `queryable`       | Data exposed for data binding                                   |
| `system`          | Advanced settings (localization, marker configuration)          |

### Organize component data

When you design a component, separate configuration, reusable component data, and event payloads. This keeps the manifest easier to read and makes the resulting Studio UI more predictable.

Use this table to find where each kind of data is declared, then jump to the relevant reference section below:

| Use case                                 | Declare in `pandasuite.json`                                                                                    | Runtime API                                                                                                                        | Appears in Studio                                                               |
| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- |
| Configure the component from Studio      | [`properties`](https://docs.pandasuite.com/essentials/components/custom-components/#properties)                 | [`PandaBridge.UPDATED`](https://docs.pandasuite.com/essentials/components/custom-components/#updating-properties-and-exposed-data) | Right-side property panel                                                       |
| Expose live data for data binding        | [Root `queryable`](https://docs.pandasuite.com/essentials/components/custom-components/#exposed-data-queryable) | [`PandaBridge.UPDATED`](https://docs.pandasuite.com/essentials/components/custom-components/#updating-properties-and-exposed-data) | Data binding window under **From a component**                                  |
| Send contextual data when an event fires | [`events[].queryable`](https://docs.pandasuite.com/essentials/components/custom-components/#events-triggers)    | [`PandaBridge.send()`](https://docs.pandasuite.com/essentials/components/custom-components/#sending-events)                        | Action flows triggered by that event                                            |
| Declare events discovered at runtime     | — (sent by the component, not declared)                                                                         | [`PandaBridge.UPDATED`](https://docs.pandasuite.com/essentials/components/custom-components/#declare-events-discovered-at-runtime) | **Actions** panel of the component, arguments under **From the selected event** |

> **Note**
>
> `pandasuite.json` defines the schema Studio reads. The linked sections below explain how to declare each type of data and which PandaBridge API to use at runtime.

### Properties

Properties appear in Studio's property panel. Each property has a type that determines its input UI.

```json
{
  "properties": [
    {
      "id": "url",
      "name": "URL",
      "locale_name": { "fr_FR": "URL" },
      "type": "String",
      "value": "https://example.com",
      "bindable": true,
      "required": true
    },
    {
      "id": "autoplay",
      "name": "Autoplay",
      "locale_name": { "fr_FR": "Lecture automatique" },
      "type": "Boolean",
      "value": false
    }
  ]
}
```

#### Property types

| Type                      | Description                    | UI Control       |
| ------------------------- | ------------------------------ | ---------------- |
| `String`                  | Text value                     | Text input       |
| `Boolean`                 | True/false                     | Checkbox         |
| `Integer`                 | Whole number                   | Number input     |
| `Float`                   | Decimal number                 | Number input     |
| `Color`                   | Color value                    | Color picker     |
| `Dropdown`                | Selection from list            | Dropdown menu    |
| `Image`, `Video`, `Audio` | Media resource                 | File picker      |
| `Bind`                    | Data binding expression        | Binding editor   |
| `JSON`                    | Structured data                | JSON editor      |
| `Code`                    | Code with syntax highlighting  | Code editor      |
| `Selection`               | Reference to another component | Component picker |

#### Property options

| Option        | Type              | Description                                      |
| ------------- | ----------------- | ------------------------------------------------ |
| `id`          | string            | Unique identifier (used in code)                 |
| `name`        | string            | Display name in Studio                           |
| `locale_name` | object            | Localized names (e.g., `{ "fr_FR": "Nom" }`)     |
| `type`        | string            | Property type                                    |
| `value`       | any               | Default value                                    |
| `bindable`    | boolean           | Allow data binding                               |
| `i18n`        | boolean           | Store a separate value for each project language |
| `required`    | boolean           | Mark as required                                 |
| `hidden`      | string \| boolean | Conditional visibility expression                |
| `placeholder` | string            | Placeholder text for inputs                      |
| `separator`   | boolean           | Show visual separator above                      |

#### Localized property values

Set `i18n: true` when a property should keep a different value for each project language. Use `locale_name` to localize the property label in Studio; use `i18n` to localize the value entered by editors.

```json
{
  "properties": [
    {
      "id": "title",
      "name": "Title",
      "locale_name": { "fr_FR": "Titre" },
      "type": "String",
      "value": "Welcome",
      "i18n": true
    }
  ]
}
```

#### Conditional visibility

Use the `hidden` property to show/hide properties based on other values:

```json
{
  "properties": [
    {
      "id": "mode",
      "name": "Mode",
      "type": "Dropdown",
      "items": [
        { "id": "simple", "name": "Simple" },
        { "id": "advanced", "name": "Advanced" }
      ],
      "value": "simple"
    },
    {
      "id": "advancedOption",
      "name": "Advanced Option",
      "type": "String",
      "hidden": "properties.mode.value != 'advanced'"
    }
  ]
}
```

#### Dropdown items

```json
{
  "id": "quality",
  "name": "Quality",
  "type": "Dropdown",
  "value": "medium",
  "items": [
    { "id": "low", "name": "Low", "locale_name": { "fr_FR": "Basse" } },
    { "id": "medium", "name": "Medium", "locale_name": { "fr_FR": "Moyenne" } },
    { "id": "high", "name": "High", "locale_name": { "fr_FR": "Haute" } }
  ]
}
```

#### Numeric constraints

```json
{
  "id": "speed",
  "name": "Speed",
  "type": "Float",
  "value": 1.0,
  "restrict": {
    "min": 0.1,
    "max": 10.0,
    "step": 0.1
  }
}
```

### Events (triggers)

Events let your component trigger actions on other components. Define queryable data that becomes available when the event fires.

```json
{
  "events": [
    {
      "id": "onItemSelected",
      "name": "Item selected",
      "locale_name": { "fr_FR": "Élément sélectionné" },
      "queryable": {
        "itemId": "example-id",
        "__desc_itemId": {
          "name": "Item ID",
          "locale_name": { "fr_FR": "ID de l'élément" }
        },
        "itemName": "Example Name",
        "__desc_itemName": {
          "name": "Item name",
          "locale_name": { "fr_FR": "Nom de l'élément" }
        }
      }
    }
  ]
}
```

The `queryable` object defines data exposed when the event fires. Use `__desc_` prefixed keys to provide metadata for each field.

#### Let the author answer the event

An event can also declare a **response**: the values the author is expected to send back to whatever fired it. Add a `response` object whose `params` array has the shape of an action's parameters.

```json
{
  "id": "searchPages",
  "name": "Search pages",
  "queryable": { "query": "", "__desc_query": { "name": "Query" } },
  "response": {
    "params": [
      {
        "id": "result",
        "name": "Response",
        "locale_name": { "fr_FR": "Réponse" },
        "type": "JSON",
        "bindable": true
      }
    ]
  }
}
```

Declaring it is what makes the [Return a value](https://docs.pandasuite.com/essentials/interactions/interactive-actions/#return-a-value) action available on that event: the author binds your parameters to anything the project produces, and the values reach you as the answer to that specific call.

Send the event with a callback to receive it. The callback needs `pandasuite-bridge` 5.1.0 or later, which mints and carries the key that ties an answer to its call, so two calls fired in the same moment settle independently:

```javascript
PandaBridge.send('searchPages', [{ query: 'pricing' }], (outcome) => {
  if (outcome?.status === 'failed') {
    // an action of the list failed; outcome.error describes it
    return;
  }
  // outcome.value holds one entry per response param, keyed by its id
  const answer = outcome?.value?.result;
});
```

A runtime that predates the callback never answers, and the call stays pending on purpose rather than acknowledging a value the author never computed. Give your response params ids of your own: `delay`, `bindings`, `marker`, anything starting with `__ps_`, and any id that collides with a key of the event's `queryable` are reserved and never offered to the author.

### Actions

Actions are operations that other components can trigger on your component. Actions can have parameters.

An action can also declare a **result**: what it hands back to the actions placed below it in the same list. Add a `queryable` object to the action, with the same shape as an event's, and the author can read it through [From a previous action](https://docs.pandasuite.com/essentials/data/data-binding/#from-a-previous-action) in the binding window. Binding it makes the consuming action wait until your action settles.

```json
{
  "actions": [
    {
      "id": "setVolume",
      "name": "Set volume",
      "locale_name": { "fr_FR": "Définir le volume" },
      "params": [
        {
          "id": "level",
          "name": "Volume level",
          "locale_name": { "fr_FR": "Niveau de volume" },
          "type": "Float",
          "value": 1.0,
          "restrict": { "min": 0, "max": 1 }
        }
      ]
    },
    {
      "id": "reset",
      "name": "Reset",
      "locale_name": { "fr_FR": "Réinitialiser" }
    }
  ]
}
```

### Resources

Resources are media files users can attach to your component.

```json
{
  "resources": [
    {
      "id": "backgroundImage",
      "name": "Background image",
      "locale_name": { "fr_FR": "Image de fond" },
      "type": "Image",
      "bindable": true,
      "localization": true
    },
    {
      "id": "dataArchive",
      "name": "Data archive",
      "type": "Zip",
      "required": true
    }
  ]
}
```

Resource types: `Image`, `Video`, `Audio`, `Zip`, `JSON`

Set `localization: true` to allow different resources per language.

### Synchronization

Synchronization groups let components share state (like playback position or scroll position).

```json
{
  "synchronization": [
    {
      "id": "playbackPosition",
      "name": "Synchronize playback",
      "locale_name": { "fr_FR": "Synchroniser la lecture" }
    }
  ]
}
```

### Exposed data (queryable)

The root `queryable` object exposes data that other components can access through data binding. Unlike `events[].queryable`, these fields exist independently of any event and stay available from the component itself in the data binding picker.

```json
{
  "queryable": {
    "progress": 0,
    "__desc_progress": {
      "name": "Progress",
      "locale_name": { "fr_FR": "Progression" },
      "type": "number"
    },
    "isPlaying": false,
    "__desc_isPlaying": {
      "name": "Is playing",
      "locale_name": { "fr_FR": "En lecture" }
    }
  }
}
```

#### Update exposed data at runtime

Declaring `queryable` in the manifest defines which fields exist. To push their current values after a login, an API response, or any user action, send an `UPDATED` message:

```javascript
PandaBridge.send(PandaBridge.UPDATED, {
  queryable: {
    progress: 42,
    isPlaying: true
  }
});
```

Once the metadata is loaded, these fields appear in the data binding window under **From a component**. Sending `UPDATED` refreshes their current values. If you add or rename fields in `pandasuite.json`, refresh the metadata in Studio first so the new schema is loaded.

### System configuration

The `system` object controls advanced behaviors.

```json
{
  "system": {
    "localization": true,
    "markers": [
      {
        "params": [
          {
            "id": "timestamp",
            "name": "Time",
            "type": "Float"
          }
        ]
      }
    ],
    "actions": [
      {
        "id": "toMarker",
        "name": "Go to marker"
      }
    ],
    "events": [
      {
        "id": "triggerMarker",
        "name": "Marker reached"
      }
    ]
  }
}
```

Set `"markers": false` to disable the marker system entirely.

## PandaBridge JavaScript API

The JavaScript library handles communication between your component and PandaSuite.

### Installation

```bash
npm install pandasuite-bridge
```

### Initialization

```javascript
import PandaBridge from 'pandasuite-bridge';

PandaBridge.init(() => {
  console.log('Bridge ready');
});
```

### Receiving properties and data

```javascript
// Called once when component loads
PandaBridge.onLoad(({ properties, markers, resources }) => {
  console.log('Properties:', properties);
  console.log('Markers:', markers);
  console.log('Resources:', resources);

  // Initialize your component with these values
  initComponent(properties);
});

// Called when properties change
PandaBridge.onUpdate(({ properties, markers, resources }) => {
  console.log('Updated properties:', properties);

  // Update your component
  updateComponent(properties);
});
```

### Updating properties and exposed data

Use `PandaBridge.UPDATED` when your component needs to push new values back to PandaSuite:

```javascript
// Update values declared in the `properties` array
PandaBridge.send(PandaBridge.UPDATED, {
  properties: [
    { id: 'url', value: 'https://example.com/player' },
    { id: 'autoplay', value: true }
  ]
});

// Update values declared in the root `queryable` object
PandaBridge.send(PandaBridge.UPDATED, {
  queryable: {
    progress: 42,
    isPlaying: true
  }
});
```

Use `properties` for values shown in the right-side panel. Use `queryable` for values you want to reuse elsewhere in your project through **From a component** data binding.

#### Declare events discovered at runtime

Some components only learn their events from an external system: the Client Tools of an AI agent, the events of a 3D scene, the hooks of a form. Instead of listing them in `pandasuite.json`, send them in the `events` key of the same message: one event, or the complete list. Each entry has the shape of an entry of the manifest's `events` array (`id`, `name`, `description`, `queryable` for the arguments, `response` when the author is expected to answer), and Studio treats it exactly like a manifest event: it appears in the **Actions** panel of the component and its arguments are offered under **From the selected event** in the data binding window. The [D-ID Agent](https://docs.pandasuite.com/essentials/components/d-id-agent/) component works this way: it declares each Client Tool of your D-ID agent as an event of the component.

```javascript
PandaBridge.send(PandaBridge.UPDATED, {
  events: [
    {
      id: 'tool_8f3a',                       // stable identifier, never the display name
      name: 'search_pages',
      description: 'Search the pages of the project',
      queryable: { query: '', __desc_query: { name: 'Query', type: 'String' } }
    }
  ]
});
```

Rules to keep in mind:

- `events` accepts two forms. An **array** is the **complete** list of the events your component discovers: an event missing from the list is removed from the project **together with the actions attached to it**, exactly as when an event disappears from the manifest, and an empty array removes every declared event. A single **object** adds one event, or redefines an existing one in place, and leaves the other events untouched: use it to declare or update events one at a time, without rebuilding your list. Only send a list once it is known; to leave the declared events untouched, omit the key rather than sending a partial or empty list. The events of your manifest are not concerned: do not list them, they stay as they are.
- Every entry needs an `id`, a non-empty string: a list in which an entry has no `id` is ignored as a whole, so a malformed entry never removes an event. Use stable identifiers (the identifier of the tool, not its name) so that a renamed event keeps its actions, and do not reuse the identifier of a manifest event or action.
- At runtime, trigger a declared event like any other: `PandaBridge.send('tool_8f3a', [{ query: 'pricing' }])`. The values you send are the ones offered under **From the selected event**. PandaSuite does not send the declared events back to your component: keep the list in one of your properties if you need it at runtime.

### Sending events

Use `PandaBridge.send()` to emit events defined in your manifest:

```javascript
// Trigger an event
PandaBridge.send('onItemSelected', {
  itemId: 'item-123',
  itemName: 'Selected Item'
});
```

### Listening for actions

```javascript
// Listen for a specific action
PandaBridge.listen('setVolume', ([level]) => {
  setVolume(level);
});

// Listen for all actions
PandaBridge.listen((action, args) => {
  console.log('Action received:', action, args);
});
```

### Synchronization

```javascript
// Send sync data
PandaBridge.send(PandaBridge.SYNCHRONIZE, [currentValue, 'syncGroupId']);

// Receive sync data
PandaBridge.synchronize('syncGroupId', (value) => {
  updateFromSync(value);
});
```

### Markers (snapshots)

```javascript
// Provide snapshot data when requested
PandaBridge.getSnapshotData(() => {
  return {
    timestamp: getCurrentTime(),
    customData: getComponentState()
  };
});

// Restore from snapshot data
PandaBridge.setSnapshotData(({ data }) => {
  if (data) {
    setCurrentTime(data.timestamp);
    restoreState(data.customData);
  }
});
```

### Resources

```javascript
// Resolve a resource path
const imagePath = PandaBridge.resolvePath('backgroundImage', '/default.png');

// Get full resource object
const resource = PandaBridge.resolveResource('backgroundImage');
if (resource) {
  // Access responsive variants
  const largePath = resource.srcsets?.['1280w'] || resource.path;
}
```

### Utility properties

```javascript
// Check if running in Studio (vs. published app)
if (PandaBridge.isStudio) {
  // Show placeholder or preview mode
}

// Get current language
const language = PandaBridge.currentLanguage;
```

### Complete example

```javascript
import PandaBridge from 'pandasuite-bridge';

let state = { count: 0 };

PandaBridge.init(() => {
  // Load initial data
  PandaBridge.onLoad(({ properties }) => {
    state.count = properties.initialCount || 0;
    render();
  });

  // Handle updates
  PandaBridge.onUpdate(({ properties }) => {
    if (properties.initialCount !== undefined) {
      state.count = properties.initialCount;
      render();
    }
  });

  // Handle actions
  PandaBridge.listen('increment', () => {
    state.count++;
    render();
    PandaBridge.send('onCountChanged', { count: state.count });
  });

  PandaBridge.listen('reset', () => {
    state.count = 0;
    render();
    PandaBridge.send('onCountChanged', { count: state.count });
  });

  // Snapshot support
  PandaBridge.getSnapshotData(() => ({ count: state.count }));
  PandaBridge.setSnapshotData(({ data }) => {
    if (data) {
      state.count = data.count;
      render();
    }
  });
});

function render() {
  document.getElementById('counter').textContent = state.count;
}
```

## PandaBridge React API

The React library provides hooks for easier integration with React components.

### Installation

```bash
npm install pandasuite-bridge-react
```

### Basic usage

```jsx
import { PandaBridgeRoot, usePandaBridge } from 'pandasuite-bridge-react';

function MyComponent() {
  const { properties, setProperty, resources } = usePandaBridge({
    actions: {
      increment: () => console.log('Increment called'),
      reset: () => console.log('Reset called')
    }
  });

  return (
    <div>
      <p>Count: {properties?.count || 0}</p>
      <button onClick={() => setProperty('count', (properties?.count || 0) + 1)}>
        Increment
      </button>
    </div>
  );
}

export default function App() {
  return (
    <PandaBridgeRoot>
      <MyComponent />
    </PandaBridgeRoot>
  );
}
```

### usePandaBridge hook

```jsx
const {
  properties,      // Current property values
  markers,         // Marker data
  resources,       // Resolved resources (by ID)
  setProperty,     // Update a single property
  setProperties,   // Update multiple properties
  addActions       // Add action handlers
} = usePandaBridge({
  actions: {
    actionName: (args) => { /* handle action */ }
  },
  synchronization: {
    syncGroupId: (value) => { /* handle sync */ }
  },
  markers: {
    getSnapshotDataHook: () => ({ /* snapshot data */ }),
    setSnapshotDataHook: (data) => { /* restore from snapshot */ }
  },
  component: {
    getScreenshotHook: (callback) => { /* custom screenshot */ },
    onLanguageChanged: (lang) => { /* handle language change */ }
  }
});
```

### Accessing resources

```jsx
function ImageDisplay() {
  const { resources } = usePandaBridge();

  const backgroundImage = resources?.backgroundImage;

  if (!backgroundImage) {
    return <div>No image selected</div>;
  }

  return <img src={backgroundImage.path} alt="Background" />;
}
```

### Built-in UI components

The React library includes pre-styled components:

```jsx
import {
  Button,
  Input,
  Dropdown,
  Checkbox,
  Alert,
  Tabs,
  Tab,
  Loader
} from 'pandasuite-bridge-react';

function SettingsPanel() {
  return (
    <div>
      <Input placeholder="Enter value" />
      <Dropdown>
        <Dropdown.Item value="a">Option A</Dropdown.Item>
        <Dropdown.Item value="b">Option B</Dropdown.Item>
      </Dropdown>
      <Checkbox>Enable feature</Checkbox>
      <Button primary>Save</Button>
      <Alert type="info">Settings saved successfully</Alert>
    </div>
  );
}
```

## Testing and publishing

### Development setup

Start with a boilerplate project:

- **JavaScript**: [js-boilerplate-component](https://github.com/pandasuite/js-boilerplate-component)
- **React**: [react-boilerplate-component](https://github.com/pandasuite/react-boilerplate-component)

### Local development

```bash
# Install dependencies
npm install

# Start development server
npm start
```

This starts a local server. In PandaSuite Studio:

1. Add a **Web** component
2. Enter your local URL (e.g., `http://localhost:3000`)
3. Under **Metadata**, tap/click **Refresh** to load your `pandasuite.json` manifest

### Building for production

```bash
# Create component zip
npm run build
```

This creates `pandasuite-component.zip`. Drag this file onto your PandaSuite project to add the component.

### Publishing to GitHub

```bash
# Create a GitHub release
npm run release
```

This publishes your component as a GitHub release, making it available for others to use.

## Best practices

- **Start from a boilerplate**: use the official [JavaScript](https://github.com/pandasuite/js-boilerplate-component) or [React](https://github.com/pandasuite/react-boilerplate-component) templates to ensure proper project structure
- **Keep components focused**: each component should do one thing well; split complex functionality into multiple components
- **Use meaningful IDs**: property, event, and action IDs should be descriptive and consistent (e.g., `onItemSelected` rather than `event1`)
- **Provide localization**: add `locale_name` for all user-facing strings to support French and English users
- **Handle Studio mode**: use `PandaBridge.isStudio` to show appropriate previews or placeholders when editing in Studio
- **Test across platforms**: verify your component works on iOS, Android, and web exports

## Limitations

- Custom components require web development skills (HTML, CSS, JavaScript)
- Components run inside a web view, which may limit access to some native device APIs
- Large component bundles can impact app loading performance

## Example components

Explore existing custom components for reference:

- [react-media-player-component](https://github.com/pandasuite/react-media-player-component): URL-based media player
- [react-qr-reader-component](https://github.com/pandasuite/react-qr-reader-component): QR code scanner
- [after-effects-component](https://github.com/pandasuite/after-effects-component): Lottie animations
- [More components on GitHub](https://github.com/pandasuite?tab=repositories)

## See also

- [Web component](https://docs.pandasuite.com/essentials/components/web/): embed web content without PandaBridge
- [Data binding](https://docs.pandasuite.com/essentials/data/data-binding/): connect data to component properties
- [Interactive actions](https://docs.pandasuite.com/essentials/interactions/interactive-actions/): trigger behaviors between components
- [Triggers](https://docs.pandasuite.com/essentials/interactions/triggers/): events that activate actions
