This guide explains how Tailor plugins can dynamically inject UI components (HTML, CSS) into the frontend from Python.
Plugins can inject custom UI elements without requiring any frontend code changes. All UI is sent dynamically via WebSocket UI_COMMAND events.
Inject CSS styles for your plugin. Styles are scoped by plugin ID.
self.brain.emit_to_frontend(
event_type=EventType.UI_COMMAND,
data={
"action": "inject_css",
"plugin_id": "my-plugin", # Unique plugin identifier
"css": """
.my-plugin-container {
background: #f5f5f5;
padding: 12px;
border-radius: 8px;
}
.my-plugin-btn {
color: var(--accent-primary);
}
"""
}
)Notes:
- CSS is injected into
<head>as a<style>tag with IDplugin-css-{plugin_id} - Existing styles with same ID are replaced (hot reload support)
- Use CSS variables for theme compatibility:
var(--text-primary),var(--bg-card), etc.
Inject HTML into any target element in the DOM.
self.brain.emit_to_frontend(
event_type=EventType.UI_COMMAND,
data={
"action": "inject_html",
"id": "my-element-id", # Optional: ID for the root element
"target": ".message-content", # CSS selector for target element
"position": "beforeend", # Where to insert (see positions below)
"html": """
<div id="my-element-id" class="my-plugin-container">
<span>Hello from plugin!</span>
<button class="my-plugin-btn" onclick="window.myPluginAction()">
<i data-lucide="star"></i> Click me
</button>
</div>
"""
}
)Position Options:
| Position | Description |
|---|---|
beforeend |
Inside target, after last child (default) |
afterbegin |
Inside target, before first child |
beforebegin |
Before target element |
afterend |
After target element |
Notes:
- If
idis provided and element exists, it will be updated instead of duplicated - Lucide icons (
<i data-lucide="icon-name">) are auto-initialized - Use
onclick="window.myFunction()"for interactivity
Remove an injected HTML element from the DOM.
self.brain.emit_to_frontend(
event_type=EventType.UI_COMMAND,
data={
"action": "remove_html",
"id": "my-element-id" # Element ID to remove
# OR
"selector": ".my-plugin-item" # CSS selector (fallback)
}
)Update the innerHTML of an existing element.
self.brain.emit_to_frontend(
event_type=EventType.UI_COMMAND,
data={
"action": "update_html",
"id": "my-element-id", # Element ID
"html": "<span>New content!</span>"
}
)For interactive elements, inject a <script> tag that defines global functions:
def _inject_plugin_js(self):
"""Inject JavaScript helpers for plugin interactivity."""
js_code = '''
<script id="my-plugin-js">
window.myPluginAction = function() {
alert('Button clicked!');
};
window.myPluginSubmit = async function(value) {
// Call backend command
await window.request('execute_command', {
command: 'my-plugin.handle_submit',
args: { value: value }
});
};
</script>
'''
self.brain.emit_to_frontend(
event_type=EventType.UI_COMMAND,
data={
"action": "inject_html",
"id": "my-plugin-js",
"target": "head",
"position": "beforeend",
"html": js_code
}
)| Target | Description |
|---|---|
#chat-area |
Main chat container |
#chat-messages |
Messages list |
[data-message-id='msg-1'] |
Specific message by ID |
.message-toolbar-container |
Message toolbar area |
.chat-composer |
Chat input area |
head |
Document head (for scripts) |
- Unique IDs: Always use unique, prefixed IDs:
my-plugin-container,my-plugin-btn - CSS Variables: Use theme variables for colors to support dark/light modes
- Idempotent Injection: Check if elements exist before injecting to avoid duplicates
- Cleanup: Remove injected elements in
on_unload()if needed - Error Handling: Wrap interactivity in try/catch blocks
# In your plugin's on_client_connected():
async def on_client_connected(self) -> None:
# 1. Inject CSS
self.brain.emit_to_frontend(
event_type=EventType.UI_COMMAND,
data={"action": "inject_css", "plugin_id": self.name, "css": MY_CSS}
)
# 2. Register toolbar button
self.brain.emit_to_frontend(
event_type=EventType.UI_COMMAND,
data={
"action": "register_action",
"id": "my-action",
"icon": "star",
"label": "My Action",
"location": "message-actionbar",
"command": "my-plugin.do_action"
}
)
# In your command handler:
async def _handle_do_action(self, message_id, content, **kwargs):
# 3. Inject UI
self.brain.emit_to_frontend(
event_type=EventType.UI_COMMAND,
data={
"action": "inject_html",
"id": f"my-result-{message_id}",
"target": f"[data-message-id='{message_id}'] .message-toolbar-container",
"position": "beforebegin",
"html": f'<div id="my-result-{message_id}">Result: {content[:50]}...</div>'
}
)See example-vault/plugins/summarizer/main.py for a complete working example that:
- Injects CSS styles on connect
- Injects interactive HTML with expand/collapse
- Injects JavaScript for client-side interactivity
- Uses
remove_htmlandupdate_htmlfor menu actions