npm package: @swis/genui-widgets
Description: Standalone Vue 3 widget renderer compatible with OpenAI ChatKit widget JSON.
Author: Joris Meijer
Latest version: 1.2.0

# `@swis/genui-widgets`


[![Software License](https://img.shields.io/packagist/l/swisnl/mcp-client.svg)](LICENSE.md)
[![Buy us a tree](https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-lightgreen.svg)](https://plant.treeware.earth/swisnl/genui-widgets)
[![Made by SWIS](https://img.shields.io/badge/%F0%9F%9A%80-made%20by%20SWIS-%230737A9.svg)](https://www.swis.nl)

Render OpenAI ChatKit widget JSON in any web page with a single function call.

Pass a ChatKit widget payload directly to `render()` and it displays. No ChatKit dependency, no framework lock-in.

![widget.png](docs/assets/widget.png)

## Installation

```bash
npm install @swis/genui-widgets
```

## IIFE bundle (no build step)

The standalone bundle includes Vue and injects CSS automatically.

```html
<div id="widget"></div>
<script src="/dist/genui-widgets.js"></script>
<script>
  GenUIWidgets.render(
    document.getElementById('widget'),
    chatKitPayload,
    { format: 'chatkit' }
  );
</script>
```

## Render an OpenAI widget payload

```ts
import { render } from '@swisnl/genui-widgets';
import '@swisnl/genui-widgets/styles';

const container = document.getElementById('widget');

render(container, chatKitPayload, { format: 'chatkit' });

container.addEventListener('genui-action', (e) => {
  const { action, payload, formData } = e.detail;
  console.log('Action:', action);   // { type: 'submit', payload: { ... } }
  console.log('Payload:', payload); // shortcut for action.payload
  console.log('Form:', formData);   // { name: 'John', email: 'john@example.com' }

  // Register async work — buttons stay in loading state until resolved
  e.detail.waitUntil(
    fetch('/api/handle', { body: JSON.stringify({ action, formData }) })
  );

  // Optionally stop other listeners from running
  // e.stopImmediatePropagation();
});
```

## Jinja templates

Templates can be Jinja strings that resolve to widget JSON at render time:

```ts
render(container, `{"type":"Card","children":[{"type":"Title","value":{{ title | tojson }}}]}`, {
  templateContext: { title: 'Hello' },
});
```

## `.widget` files

Extract the template from a `.widget` file and pass it directly to `render()`:

```ts
import { extractTemplateFromWidgetFile, render } from '@swisnl/genui-widgets';

const template = extractTemplateFromWidgetFile(await file.text());
render(container, template);
```


That's it. Pass the raw payload from the OpenAI response and the widget renders.

## Vue component

```vue
<script setup lang="ts">
import { DynamicWidget, fromChatKit } from '@swisnl/genui-widgets';
import type { ActionEventDetail } from '@swisnl/genui-widgets';
import '@swisnl/genui-widgets/styles';

const template = fromChatKit(chatKitPayload);

function handleAction(e: CustomEvent<ActionEventDetail>) {
  console.log('Action:', e.detail.action);
}
</script>

<template>
  <div @genui-action="handleAction">
    <DynamicWidget :template="template" />
  </div>
</template>
```

## Updating and destroying

`render()` returns an instance you can update or clean up:

```ts
const widget = render(container, chatKitPayload, { format: 'chatkit' });

// Swap in a new payload
widget.update(newPayload);

// Clean up
widget.destroy();
```

## Theming

Widgets are styled through `--genui-*` CSS custom properties, all scoped to the `.genui-widget-root` container element. The library ships two ready-made themes and exposes the full API for custom themes.

### Built-in themes

```ts
import { defaultTheme, darkTheme, render } from '@swisnl/genui-widgets';

// Light (default — applied automatically when no theme is passed)
render(container, payload, { theme: defaultTheme });

// Dark
render(container, payload, { theme: darkTheme });
```

### Overriding with CSS variables

Because all tokens are plain CSS custom properties on `.genui-widget-root`, you can override them directly in a stylesheet without touching JavaScript at all:

```css
/* Target all widget roots on the page */
.genui-widget-root {
  --genui-surface: #f8f4ff;
  --genui-background: #f8f4ff;
  --genui-text-primary: #1a0040;
  --genui-primary-60: #7c3aed;
  --genui-primary-70: #6d28d9;
  --genui-border-default: #e2e8f0;
  --genui-base-size: 0.9rem;
}

/* Or scope overrides to a specific container */
#my-widget .genui-widget-root {
  --genui-surface: #1e1e2e;
  --genui-text-primary: #cdd6f4;
}
```

### Custom theme

Use `createTheme()` to merge overrides on top of a base theme:

```ts
import { createTheme, defaultTheme, render } from '@swisnl/genui-widgets';

render(container, payload, {
  theme: createTheme(defaultTheme, {
    surface: '#f8f4ff',
    background: '#f8f4ff',
    textPrimary: '#1a0040',
    palettes: {
      primary: { 60: '#7c3aed', 70: '#6d28d9' },
    },
  }),
});
```

### Semantic color palettes

Each semantic color (`primary`, `secondary`, `success`, `danger`, `warning`, `info`, `discovery`, `caution`) exposes ten lightness steps (`5` → `90`) as CSS variables:

```
--genui-primary-60       /* hex value */
--genui-primary-60-rgb   /* "R, G, B" for alpha compositing */
```

Override individual steps without replacing the whole palette:

```ts
createTheme(defaultTheme, {
  palettes: {
    primary: { 60: '#2563eb', 70: '#1d4ed8' },
    success: { 50: '#10b981' },
  },
});
```

### Raw CSS variable overrides

Use the `overrides` escape hatch to set any `--genui-*` variable directly:

```ts
createTheme(defaultTheme, {
  overrides: {
    'base-size': '0.9rem',
    '--genui-border-default': '#e2e8f0',
  },
});
```

Keys are automatically prefixed with `--genui-` when not already prefixed.

### Applying a theme in Vue with `useTheme`

```vue
<script setup lang="ts">
import { ref } from 'vue';
import { useTheme, darkTheme } from '@swisnl/genui-widgets';

const container = ref<HTMLElement | null>(null);
const { theme } = useTheme(container, darkTheme);

// Reactively update any token — the container updates automatically
theme.value = { ...theme.value, surface: '#1a1a2e' };
</script>

<template>
  <div ref="container">
    <DynamicWidget :template="widget" />
  </div>
</template>
```

### Applying a theme manually

```ts
import { applyTheme, createTheme, defaultTheme } from '@swisnl/genui-widgets';

applyTheme(document.getElementById('widget'), createTheme(defaultTheme, {
  textPrimary: '#1a0040',
}));
```

### Updating the theme at runtime

```ts
const widget = render(container, payload, { format: 'chatkit' });

widget.setTheme(darkTheme);
```

## Widget types

`Box` `Card` `Button` `Text` `Title` `Markdown` `Image` `Form` `Input` `Textarea` `Select` `Checkbox` `RadioGroup` `DatePicker` `Badge` `ListView` `ListViewItem` `Divider` `Spacer` `Row` `Col` `Label` `Caption` `Icon`

## Development

```bash
npm install
npm run demo      # run the demo
npm run build     # build both outputs
npm run lint
npm run typecheck
npm test
npm run test:visual
npm run test:visual:update   # refresh visual baselines
npm run test:visual:playwright-image
npm run test:visual:update:playwright-image   # refresh Linux baselines in the CI image
```

Visual baselines are stored per OS with prefixes like `macos-` and `linux-`. If you need to refresh the screenshots used by GitHub Actions, run the Playwright image update command so the baselines are regenerated inside `mcr.microsoft.com/playwright:v1.58.2-noble`.

## Build output

| File | Description |
|---|---|
| `dist/genui-widgets.esm.js` | ESM build for bundlers |
| `dist/genui-widgets.css` | Stylesheet for ESM consumers |
| `dist/genui-widgets.js` | Self-contained browser bundle with CSS injection |


## Changelog

Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.

## Contributing

Please see [CONTRIBUTING](https://github.com/spatie/.github/blob/main/CONTRIBUTING.md) for details.

## Security Vulnerabilities

Please review [our security policy](../../security/policy) on how to report security vulnerabilities.

## Credits

- [Joris Meijer](https://github.com/jormeijer)
- [All Contributors](../../contributors)

## License

This package is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).

This package is [Treeware](https://treeware.earth). If you use it in production, then we ask that you [**buy the world a tree**](https://plant.treeware.earth/swisnl/mcp-client) to thank us for our work. By contributing to the Treeware forest you’ll be creating employment for local families and restoring wildlife habitats.

## SWIS :heart: Open Source

[SWIS](https://www.swis.nl) is a web agency from Leiden, the Netherlands. We love working with open source software.