Chativa is an open-source, framework-agnostic chat widget built on Web Components (LitElement). Drop a single <script> tag into any page — React, Vue, Angular, or plain HTML — and get a fully functional, themeable chat interface.
Connect to any backend via pluggable connectors (WebSocket, SignalR, Azure Bot DirectLine, or your own), render rich message types (cards, carousels, images, video), stream Generative UI components inline in the chat, and extend behavior with a middleware extension pipeline.
- Features
- Quick Start
- Configuration
- Connectors
- Built-in Message Types
- Custom Message Types
- Generative UI (GenUI)
- Extensions
- Slash Commands
- File Upload
- Message History
- Message Status
- Theming
- i18n
- Architecture
- Directory Structure
- Development
- Contributing
- License
| Feature | Description |
|---|---|
| Zero-config embed | One <script> tag + one HTML element |
| Framework-agnostic | Works in React, Vue, Angular, or plain HTML |
| Pluggable connectors | WebSocket, SignalR, Azure DirectLine, or custom |
| Built-in message types | Text/Markdown, image, card, carousel, buttons, quick replies, file, video |
| Custom message types | Register any LitElement component for any message type |
| Generative UI streaming | Stream AI-generated components (forms, charts, cards) inline |
| Extension API | Middleware hooks for analytics, transformers, slash commands |
| Slash commands | Built-in /clear + register custom commands with autocomplete |
| File upload | Optional sendFile() support per connector |
| Message history | Cursor-based pagination with scroll-to-top loading |
| Message status | Sending → Sent → Read tick indicators |
| Auto-reconnect | Exponential backoff, configurable max attempts |
| Full theming | CSS variables + JSON config object + avatar support |
| i18n ready | Built-in English & Turkish; extensible via i18next |
| Virtual scrolling | Efficient rendering for large message histories |
| TypeScript strict | Full types exported, no any |
| < 50 KB gzipped | Lightweight core bundle |
pnpm add @chativa/ui @chativa/core
# also add a connector:
pnpm add @chativa/connector-dummy<script type="module" src="https://unpkg.com/@chativa/ui/dist/chativa.js"></script>
<chat-bot-button></chat-bot-button>
<chat-iva connector="dummy"></chat-iva>import { ConnectorRegistry, chatStore } from "@chativa/core";
import { WebSocketConnector } from "@chativa/connector-websocket";
ConnectorRegistry.register(
new WebSocketConnector({ url: "wss://your-server/chat" })
);
chatStore.getState().setConnector("websocket");
chatStore.getState().setTheme({
colors: { primary: "#4f46e5" },
position: "bottom-right",
});import { chatStore } from "@chativa/core";
chatStore.getState().setTheme({
colors: {
primary: "#4f46e5",
secondary: "#6c757d",
background: "#ffffff",
text: "#212529",
border: "#dee2e6",
},
position: "bottom-right", // bottom-right | bottom-left | top-right | top-left
positionMargin: "2",
size: "medium", // small | medium | large
layout: {
width: "360px",
height: "520px",
maxWidth: "100%",
maxHeight: "100%",
},
avatar: {
bot: "https://example.com/bot-avatar.png", // URL or omit for default SVG
showBot: true,
showUser: false,
},
showMessageStatus: true, // show delivery/read ticks on user messages
allowFullscreen: true,
});Override any visual property at runtime:
chat-iva {
--chativa-primary-color: #4f46e5;
--chativa-background-color: #ffffff;
--chativa-text-color: #212529;
--chativa-border-radius: 12px;
--chativa-font-family: "Inter", sans-serif;
--chativa-button-size: 56px;
--chativa-position-x: 24px;
--chativa-position-y: 24px;
}Chativa uses a Ports & Adapters pattern. Every connector implements IConnector and is registered by name.
| Package | Connector | Description |
|---|---|---|
@chativa/connector-dummy |
DummyConnector |
Local mock — no network, great for dev/testing |
@chativa/connector-websocket |
WebSocketConnector |
Native browser WebSocket |
@chativa/connector-signalr |
SignalRConnector |
Microsoft SignalR hub |
@chativa/connector-directline |
DirectLineConnector |
Azure Bot Framework DirectLine v3 |
import { DummyConnector } from "@chativa/connector-dummy";
const connector = new DummyConnector({
replyDelay: 500, // ms before auto-reply
connectDelay: 2000, // ms to simulate handshake
});
// Helpers for testing
connector.injectMessage({ id: "1", type: "text", from: "bot", data: { text: "Hello!" } });
connector.triggerGenUI("/genui-weather");import { WebSocketConnector } from "@chativa/connector-websocket";
new WebSocketConnector({
url: "wss://your-server/chat",
protocols: ["v1"],
reconnect: true, // default true
reconnectDelay: 2000, // ms
maxReconnectAttempts: 5,
});import { SignalRConnector } from "@chativa/connector-signalr";
new SignalRConnector({
url: "https://your-server/hub",
hubName: "chat",
receiveMethod: "ReceiveMessage",
sendMethod: "SendMessage",
accessTokenFactory: () => localStorage.getItem("token") ?? "",
});import type {
IConnector,
OutgoingMessage,
MessageHandler,
IncomingMessage,
HistoryResult,
} from "@chativa/core";
import { ConnectorRegistry } from "@chativa/core";
class MyApiConnector implements IConnector {
readonly name = "my-api";
readonly addSentToHistory = true;
private messageHandler: MessageHandler | null = null;
async connect(): Promise<void> { /* open connection */ }
async disconnect(): Promise<void> { /* close connection */ }
async sendMessage(msg: OutgoingMessage): Promise<void> { /* send */ }
onMessage(cb: MessageHandler): void { this.messageHandler = cb; }
// Optional: file upload
async sendFile(file: File, metadata?: Record<string, unknown>): Promise<void> { }
// Optional: cursor-based history
async loadHistory(cursor?: string): Promise<HistoryResult> {
return { messages: [], hasMore: false };
}
// Optional: delivery/read callbacks
onMessageStatus(cb: (id: string, status: "sent" | "read") => void): void { }
// Optional: like/dislike feedback
async sendFeedback(messageId: string, value: "like" | "dislike"): Promise<void> { }
}
ConnectorRegistry.register(new MyApiConnector());<chat-iva connector="my-api"></chat-iva>Chativa ships with components for all common message types. When a connector delivers an incoming message with a matching type, the correct component renders automatically.
| Type | Component | Data Shape |
|---|---|---|
text |
DefaultTextMessage |
{ text: string } — Markdown supported |
image |
ImageMessage |
{ src: string, alt?: string, caption?: string } |
card |
CardMessage |
{ title: string, subtitle?: string, image?: string, buttons?: MessageAction[] } |
buttons |
ButtonsMessage |
{ buttons: MessageAction[] } |
quick-reply |
QuickReplyMessage |
{ actions: MessageAction[] } |
carousel |
CarouselMessage |
{ cards: { title, image?, actions? }[] } |
file |
FileMessage |
{ name: string, size?: number, url?: string, type?: string } |
video |
VideoMessage |
{ src: string, poster?: string, caption?: string } |
genui |
GenUIMessage |
{ streamId: string } — see Generative UI |
MessageAction shape: { label: string, value?: string }
All built-in types are automatically registered. You can override any with your own component via MessageTypeRegistry.
Register any LitElement component to handle a custom message type:
import { MessageTypeRegistry } from "@chativa/core";
import { ProductCardMessage } from "./ProductCardMessage";
MessageTypeRegistry.register("product-card", ProductCardMessage);When an incoming message with { type: "product-card", data: { ... } } arrives, Chativa automatically renders ProductCardMessage. All message components implement IMessageRenderer:
interface IMessageRenderer {
messageData: Record<string, unknown>;
}@chativa/genui lets connectors stream UI components inline inside chat messages. The bot can render forms, charts, progress bars, and any custom component — in real time, as data arrives.
| Name | Description | Key Props |
|---|---|---|
genui-text |
Markdown text block | content: string |
genui-card |
Card with title, description, actions | title, description, actions[] |
genui-form |
Dynamic form with validation | fields[] (text/email/number/date/checkbox/select) |
genui-alert |
Styled alert box | message, variant: info|success|warning|error |
genui-quick-replies |
Quick reply chip buttons | items[] (label, value?, icon?) |
genui-list |
Scrollable item list | items[] (title, description?) |
genui-table |
Data table | headers[], rows[][] |
genui-rating |
Star rating | value, max |
genui-progress |
Progress bar | value (0-100), variant |
Register any LitElement component as a streamable GenUI component:
import { GenUIRegistry } from "@chativa/genui";
import type { GenUIComponentAPI } from "@chativa/genui";
class WeatherWidget extends LitElement {
// GenUIComponentAPI methods are injected by GenUIMessage
sendEvent?: GenUIComponentAPI["sendEvent"];
listenEvent?: GenUIComponentAPI["listenEvent"];
tFn?: GenUIComponentAPI["tFn"];
@property({ type: String }) city = "";
@property({ type: Number }) temp = 0;
render() {
return html`<div>${this.city}: ${this.temp}°C</div>`;
}
}
GenUIRegistry.register("weather", WeatherWidget);The connector emits AIChunk objects which the engine assembles into a streaming message:
import type { AIChunk, GenUIChunkHandler } from "@chativa/core";
// In your connector:
onGenUIChunk(callback: GenUIChunkHandler): void {
this.genUICallback = callback;
}
// Emit chunks as they arrive from the server:
this.genUICallback({ type: "text", content: "Current weather:", id: "chunk-1" });
this.genUICallback({ type: "ui", component: "weather", props: { city: "Istanbul", temp: 22 }, id: "chunk-2" });
this.genUICallback({ type: "event", name: "stream_end", payload: {}, id: "chunk-3" });import { streamFromFetch } from "@chativa/genui";
await streamFromFetch("/api/chat/stream", (chunk) => {
// chunk: AIChunk — route to your connector's genUICallback
});All registered GenUI components receive these methods injected at render time:
interface GenUIComponentAPI {
sendEvent(type: string, payload: unknown): void; // send to connector
listenEvent(type: string, cb: (payload: unknown) => void): void;
tFn(key: string, fallback?: string): string; // i18n translation
onLangChange(cb: () => void): () => void; // subscribe to locale change
}Extensions hook into the message pipeline. Use them for analytics, transformers, logging, or registering custom slash commands.
import { ExtensionRegistry } from "@chativa/core";
ExtensionRegistry.install(new AnalyticsExtension({ trackingId: "UA-..." }));interface IExtension {
readonly name: string;
readonly version: string;
install(context: ExtensionContext): void;
uninstall?(): void;
onBeforeSend?(msg: OutgoingMessage): OutgoingMessage | null;
onAfterReceive?(msg: IncomingMessage): IncomingMessage | null;
onWidgetOpen?(): void;
onWidgetClose?(): void;
}Return null from onBeforeSend or onAfterReceive to block the message entirely.
ExtensionContext also exposes registerCommand(cmd) for registering slash commands from within an extension.
Chativa has a built-in /clear command. Register your own:
import { registerCommand } from "@chativa/ui";
registerCommand({
name: "help",
description: () => t("commands.help.description"), // lazy i18n supported
usage: () => t("commands.help.usage"),
execute({ args }) {
// args: string[] — words after the command name
console.log("Help requested:", args);
},
});Users type / in the chat input to see an autocomplete list of all registered commands.
Connectors that implement sendFile() automatically get a file upload button in the chat input.
class MyConnector implements IConnector {
async sendFile(file: File, metadata?: Record<string, unknown>): Promise<void> {
const form = new FormData();
form.append("file", file);
await fetch("/upload", { method: "POST", body: form });
}
}Connectors that implement loadHistory() enable scroll-to-top pagination. When the user scrolls to the top of the message list, older messages are loaded automatically.
class MyConnector implements IConnector {
async loadHistory(cursor?: string): Promise<HistoryResult> {
const res = await fetch(`/history?before=${cursor ?? ""}`);
const data = await res.json();
return {
messages: data.messages, // IncomingMessage[]
hasMore: data.hasMore,
cursor: data.nextCursor,
};
}
}Connectors that implement onMessageStatus() can push delivery/read updates for user messages. Enable the UI tick indicators via the theme:
chatStore.getState().setTheme({ showMessageStatus: true });class MyConnector implements IConnector {
onMessageStatus(cb: (id: string, status: "sent" | "read") => void): void {
this.statusCallback = cb;
}
}Status flow: sending (optimistic, immediately on send) → sent (server ack) → read (bot/user read receipt).
| Property | Values |
|---|---|
position |
bottom-right | bottom-left | top-right | top-left |
size |
small | medium | large |
positionMargin |
"1" – "5" (space scale) |
allowFullscreen |
boolean |
showMessageStatus |
boolean |
avatar.bot |
URL string or omit for default SVG |
avatar.user |
URL string or omit for default SVG |
avatar.showBot |
boolean |
avatar.showUser |
boolean |
All theme values are deeply merged — override only what you need via mergeTheme(base, overrides).
Chativa uses i18next internally. The widget ships with English (en) and Turkish (tr) translations.
import { i18n } from "@chativa/ui";
// Switch language at runtime
i18n.changeLanguage("tr");
// Add your own translations (e.g. from an extension)
i18n.addResourceBundle("de", "translation", {
"input.placeholder": "Nachricht schreiben...",
});The I18nMixin (from @chativa/core) provides reactive t(key) calls — all components auto-update when the language changes.
Chativa follows a Hexagonal (Ports & Adapters) architecture. The dependency rule is strictly enforced: inner layers never import from outer layers.
┌─────────────────────────────────────────────────────────┐
│ UI Layer │
│ @chativa/ui · @chativa/genui │
│ ChatWidget · ChatBotButton · ChatInput │
│ ChatMessageList · DefaultTextMessage · … │
│ GenUIMessage · GenUIForm · GenUICard · … │
└──────────────────────────┬──────────────────────────────┘
│ uses
┌──────────────────────────▼──────────────────────────────┐
│ Application Layer │
│ @chativa/core (application/) │
│ ChatEngine · ConnectorRegistry · MessageTypeRegistry │
│ ExtensionRegistry · SlashCommandRegistry │
│ ChatStore · MessageStore │
└────────┬──────────────────────────────┬─────────────────┘
│ depends on │ instantiates
┌────────▼──────────┐ ┌──────────────▼─────────────────┐
│ Domain Layer │ │ Infrastructure Layer │
│ @chativa/core │ │ @chativa/connector-dummy │
│ IConnector │ │ @chativa/connector-websocket │
│ IExtension │ │ @chativa/connector-signalr │
│ ISlashCommand │ │ @chativa/connector-directline │
│ IMessageRenderer │ │ [your-connector] │
│ Message · Theme │ └────────────────────────────────┘
│ GenUI types │
└───────────────────┘
Dependency rule: UI → Application → Domain ← Infrastructure
packages/
├── core/ # Domain + Application layers (@chativa/core)
│ └── src/
│ ├── domain/
│ │ ├── IConnector.ts # Connector port (interface + optional methods)
│ │ ├── IExtension.ts # Extension port
│ │ ├── ISlashCommand.ts # Slash command contract
│ │ ├── IMessageRenderer.ts
│ │ ├── Message.ts # IncomingMessage, OutgoingMessage, HistoryResult
│ │ ├── GenUI.ts # AIChunk, GenUIStreamState types
│ │ └── Theme.ts # ThemeConfig, mergeTheme, themeToCSS
│ ├── application/
│ │ ├── ChatEngine.ts
│ │ ├── ConnectorRegistry.ts
│ │ ├── MessageTypeRegistry.ts
│ │ ├── ExtensionRegistry.ts
│ │ ├── SlashCommandRegistry.ts
│ │ ├── ChatStore.ts
│ │ └── MessageStore.ts
│ └── ui/
│ └── I18nMixin.ts # Shared i18n mixin for LitElement
│
├── ui/ # LitElement chat widget (@chativa/ui)
│ └── src/
│ ├── chat-ui/
│ │ ├── ChatWidget.ts
│ │ ├── ChatBotButton.ts
│ │ ├── ChatHeader.ts
│ │ ├── ChatInput.ts
│ │ ├── ChatMessageList.ts
│ │ ├── EmojiPicker.ts
│ │ └── messages/
│ │ ├── DefaultTextMessage.ts
│ │ ├── ImageMessage.ts
│ │ ├── CardMessage.ts
│ │ ├── ButtonsMessage.ts
│ │ ├── QuickReplyMessage.ts
│ │ ├── CarouselMessage.ts
│ │ ├── FileMessage.ts
│ │ └── VideoMessage.ts
│ └── mixins/
│ └── ChatbotMixin.ts
│
├── genui/ # Generative UI streaming (@chativa/genui)
│ └── src/
│ ├── components/
│ │ ├── GenUIMessage.ts
│ │ ├── GenUITextBlock.ts
│ │ ├── GenUICard.ts
│ │ ├── GenUIForm.ts
│ │ ├── GenUIAlert.ts
│ │ ├── GenUIQuickReplies.ts
│ │ ├── GenUIList.ts
│ │ ├── GenUITable.ts
│ │ ├── GenUIRating.ts
│ │ └── GenUIProgress.ts
│ ├── registry/
│ │ └── GenUIRegistry.ts
│ └── types.ts
│
├── connector-dummy/ # Dev/test mock connector
├── connector-websocket/ # Native WebSocket connector
├── connector-signalr/ # Microsoft SignalR connector
└── connector-directline/ # Azure Bot Framework DirectLine connector
apps/
└── sandbox/ # Interactive demo application
└── src/
├── main.ts
├── components/
│ ├── WeatherWidget.ts
│ └── AIForm.ts
└── sandbox/
└── SandboxControls.ts
# Install dependencies
pnpm install
# Start sandbox (dev server)
pnpm dev
# Run all tests
pnpm test
# Run tests with coverage
pnpm test:coverage
# Type check all packages
pnpm typecheck
# Build all packages
pnpm build- LitElement 3 — Web Component base
- TypeScript — Strict mode
- Zustand — Vanilla state management
- i18next — Internationalization
- @shoelace-style/shoelace — UI primitives
- @lit-labs/virtualizer — Virtual scrolling
- Vite 7 — Build tool
- Vitest 4 — Unit & integration testing
Contributions are welcome. Please open an issue first to discuss what you'd like to change.
- Fork the repository
- Create your feature branch (
git checkout -b feat/my-feature) - Commit your changes (follow conventional commits)
- Open a pull request
See AGENTS.md for coding conventions and architecture rules.
MIT — © Hamza Agar