UI Contributions
Apps can extend the OpenClawOS dashboard by contributing UI elements. This guide covers how to add tabs, web components, and settings panels to the control UI.
Overview
Apps contribute UI elements through the capabilities.ui section in their manifest:
The UI fetches these contributions via apps.getUiManifest and renders them dynamically.
Adding Tabs
Tabs appear in the sidebar navigation, allowing apps to provide dedicated views.
Basic Tab
{
"capabilities": {
"ui": {
"tabs": [
{
"id": "dashboard",
"title": "My Dashboard",
"icon": "layout-dashboard",
"render": {
"type": "iframe",
"src": "/app/@myorg/myapp/dashboard"
}
}
]
}
}
}
Tab Configuration
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier within the app |
title |
string | Yes | Display name in sidebar |
icon |
string | No | Lucide icon name (default: "puzzle") |
render |
object | Yes | How to render tab content |
position |
string | No | Where to place in sidebar |
badge |
object | No | Badge count configuration |
Tab Positioning
Control where your tab appears in the sidebar:
| Position | Description |
|---|---|
top |
First position in first group |
bottom |
In "Apps" group at bottom (default) |
after:chat |
Right after the Chat tab |
after:channels |
Right after the Channels tab |
Example:
{
"id": "quick-actions",
"title": "Quick Actions",
"position": "after:chat",
"render": { "type": "iframe", "src": "/app/@myorg/myapp/quick" }
}
Render Types
iframe
Embed app-hosted content in a sandboxed iframe:
The iframe has these sandbox permissions:
allow-scriptsallow-same-originallow-formsallow-popups
component
Use a registered web component:
The component must be registered in the components section.
Icons
Tabs use Lucide icons. Common choices:
| Icon Name | Use Case |
|---|---|
layout-dashboard |
Dashboards |
bar-chart |
Analytics |
settings |
Configuration |
bell |
Notifications |
mail |
Messages |
users |
User management |
folder |
File/folder views |
puzzle |
Default/generic |
Web Components
Register custom elements for dynamic tab content or widgets.
Registering Components
{
"capabilities": {
"ui": {
"components": [
{
"tag": "my-app-dashboard",
"module": "./components/dashboard.js",
"scope": "tab"
}
]
}
}
}
Component Configuration
| Field | Type | Required | Description |
|---|---|---|---|
tag |
string | Yes | Custom element tag (must contain hyphen) |
module |
string | Yes | Path to JavaScript module |
scope |
string | Yes | Where the component is used |
Component Scopes
| Scope | Description |
|---|---|
tab |
Used as tab content |
settings |
Used in settings panels |
widget |
Embeddable widget |
global |
Loaded on app start |
Writing Components
Export your component class as the default export or as a PascalCase named export:
// components/dashboard.js
export default class MyAppDashboard extends HTMLElement {
connectedCallback() {
this.innerHTML = `<h1>My Dashboard</h1>`;
}
}
// Or named export (PascalCase of tag name)
export class MyAppDashboard extends HTMLElement { ... }
The component receives data-package-id attribute with your app's package ID.
Settings Panels
Add app-specific settings to the Config tab.
{
"capabilities": {
"ui": {
"settings": [
{
"id": "preferences",
"title": "My App Settings",
"render": {
"type": "component",
"tag": "my-app-settings"
}
}
]
}
}
}
Settings Configuration
| Field | Type | Required | Description |
|---|---|---|---|
id |
string | Yes | Unique identifier |
title |
string | Yes | Section heading |
render |
object | Yes | How to render (iframe or component) |
Badge Counts
Tabs can display badge counts (e.g., unread notifications).
Manifest Configuration
{
"id": "notifications",
"title": "Notifications",
"render": { "type": "iframe", "src": "/app/@myorg/myapp/notifications" },
"badge": {
"method": "myapp.getNotificationCount",
"interval": 30
}
}
Badge Configuration
| Field | Type | Required | Description |
|---|---|---|---|
method |
string | Yes | Gateway method to call |
interval |
number | No | Polling interval in seconds (default: 30) |
Implementing the Gateway Method
// In your app
app.registerGatewayMethod("myapp.getNotificationCount", async () => {
const count = await getUnreadNotifications();
return { count };
});
The method must return an object with a count property. Zero or missing count hides the badge.
HTTP Routes for Iframes
Serve iframe content via HTTP routes:
// Register HTTP route
app.registerHttpRoute("/dashboard", async (req, res) => {
res.setHeader("Content-Type", "text/html");
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
<style>
body { font-family: system-ui; padding: 20px; }
</style>
</head>
<body>
<h1>My Dashboard</h1>
<p>Content here...</p>
</body>
</html>
`);
});
The iframe src follows the pattern: /app/{packageId}/path
For @myorg/myapp with route /dashboard, the full URL is:
Declaring HTTP Routes
Add to capabilities:
{
"capabilities": {
"gateway": {
"httpRoutes": ["/dashboard", "/dashboard/*"]
},
"ui": {
"tabs": [...]
}
}
}
Complete Example
Full manifest with UI contributions:
{
"id": "@myorg/my-dashboard",
"name": "My Dashboard",
"version": "1.0.0",
"type": "app",
"main": "dist/index.js",
"protocol": { "version": "1.0" },
"capabilities": {
"gateway": {
"methods": ["mydash.getBadgeCount"],
"httpRoutes": ["/dashboard", "/dashboard/*", "/settings"]
},
"ui": {
"tabs": [
{
"id": "main",
"title": "Dashboard",
"icon": "layout-dashboard",
"render": {
"type": "iframe",
"src": "/app/@myorg/my-dashboard/dashboard"
},
"position": "after:chat",
"badge": {
"method": "mydash.getBadgeCount",
"interval": 30
}
}
],
"components": [
{
"tag": "mydash-widget",
"module": "./components/widget.js",
"scope": "widget"
},
{
"tag": "mydash-settings",
"module": "./components/settings.js",
"scope": "settings"
}
],
"settings": [
{
"id": "config",
"title": "Dashboard Settings",
"render": {
"type": "component",
"tag": "mydash-settings"
}
}
]
}
}
}
Best Practices
- Use meaningful IDs: Tab and setting IDs should be descriptive and unique within your app
- Choose appropriate icons: Select icons that represent your tab's purpose
- Position thoughtfully: Use
positionto place tabs where users expect them - Optimize badge polling: Set
intervalbased on data freshness needs (longer = less load) - Handle loading states: Show loading indicators in iframes and components
- Respect theming: Use CSS variables from the parent UI for consistent styling
Troubleshooting
Tab not appearing
- Verify manifest
capabilities.ui.tabsis properly formatted - Check the gateway logs for manifest loading errors
- Ensure your app is enabled and running
Component not rendering
- Confirm the tag name includes a hyphen (required for custom elements)
- Check that the module path is correct and accessible
- Look for JavaScript errors in the browser console
Badge not updating
- Verify the gateway method is registered and returns
{ count: number } - Check that the method name matches exactly
- Confirm the method doesn't throw errors
Next Steps
- App Structure - Manifest format reference
- Gateway Methods - Registering gateway methods
- Capabilities - All capability types