UI Customization Guide¶
VIBE provides three complementary ways to customize the interview UI per template:
- HTML overrides — replace layout fragments and question widgets
- CSS theming — override design tokens and component styles via
theme.css - Label overrides — change UI text (button labels, status messages, headings) via
labels.po
All customization files live in the template's ui/ directory (configured via ui.templates in config.yml). Use vibe scaffold <template_id> to generate any of these.
1. Core Files and What They Do¶
Extension note: Rows under assistant/ or review/ only apply when the corresponding extension is enabled.
| File | Purpose | Common Context |
|---|---|---|
layout/shell.html |
<html>, <head>, flash messages, global scripts, {% block content %} |
template_ui_stylesheet_href, _() |
layout/fragments/site_header.html |
Site header branding/navigation area inserted before the main content | Standard Jinja globals |
pages/interview.html |
Entry point that decides which layout to include (standard, no_preview, assistant) |
mode, template_config, template_id, version_identifier, question_panel_ctx, toolbar_ctx, preview_ctx, error_ctx, assistant_ctx, paged_ctx |
layout/standard.html |
Two-column interview layout (questions + preview) | question_panel_ctx, toolbar_ctx, preview_ctx, error_ctx, template_config, template_id, version_identifier |
layout/no_preview.html |
Single-column layout used when ui.preview: false |
Same as above |
layout/fragments/toc_sidebar.html |
TOC sidebar showing group titles and status | paged_ctx (groups, group_statuses, current_group_id) |
layout/fragments/page_navigation.html |
Previous/Next navigation buttons | paged_ctx (prev_group_id, next_group_id, current_group_title) |
assistant/layout.html |
Assistant wrapper that injects the workbench output | assistant_ctx, preview_ctx, template_config, template_id, version_identifier |
assistant/workbench.html |
Workbench UI (conversation, toolbar, draft pane) | assistant, history, toolbar_ctx, initial_error_message, current_draft_html, initial_prompt |
layout/fragments/header_branding.html |
Shows title, version, description inside #config-area |
template_config, template_id, version_identifier, _() |
layout/fragments/questions_panel.html |
Contains <form> and placeholder for question widgets |
template_id, validation_status, questions_html (Markup), optional show_questions, optional form_trigger |
layout/fragments/grouped_questions.html |
Renders grouped questions for ui.layout (paged/accordion/tabs/flat) |
grouped_ctx, layout, default_open_group_id, group_statuses, paged_ctx |
layout/fragments/toolbar.html |
Wraps save/load/download controls | template_id, toolbar_ctx (status, appendices, assistants, is_markdown, optional assistant_dev), session |
layout/fragments/error_panel.html |
Displays current validation/error messages | error_html (Markup), optional visible, optional oob |
layout/fragments/preview_panel.html |
Holds preview/assistant output (updated via HTMX) | content_html (Markup), status, optional panel_class, optional oob |
components/*.html |
Reusable fragments (reset buttons, validation summaries, placeholders) | File-specific context noted in each header comment |
htmx/*.html |
HTMX swap fragments (oob_div.html, sse_connector.html, toolbar/preview/error updates) |
Called via server responses (see create_oob_swaps and SSE endpoints) |
assistant/*.html |
Assistant-specific fragments (message bubbles, forms, SSE connector) | Context documented in each template header |
question_types/*.html |
Individual question widgets (text, bool, enum, etc.) | See Section 4 |
modals/*.html |
Modal dialogs used by the interview UI | Template-specific context noted in header |
All templates inherit the standard Jinja context (request, session, _, etc.) unless noted otherwise.
2. Include Hierarchy¶
pages/interview.html
└── extends layout/shell.html
├── include layout/fragments/site_header.html
└── block content
└── include layout/<mode>.html (standard, no_preview, or assistant/layout.html)
├── include layout/fragments/header_branding.html
├── include layout/fragments/questions_panel.html
│ └── HTMX injects question_types/* as relevance changes
├── include layout/fragments/toolbar.html
├── include layout/fragments/error_panel.html
└── include layout/fragments/preview_panel.html
For grouped layouts specifically (including `ui.layout: paged`):
layout/fragments/grouped_questions.html
└── include layout/fragments/grouped_questions/paged.html
├── include layout/fragments/toc_sidebar.html
└── include layout/fragments/page_navigation.html
layout/shell.html provides the document skeleton (doctype, <html>, <head>, JS/CSS includes, flash messages) and exposes a single {% block content %}. It also includes layout/fragments/site_header.html before the main content. pages/interview.html extends layout/shell.html and, inside that block, includes the appropriate mode-specific layout. Most authors override fragments inside the mode layout, but you can also override layout/shell.html if you need to change head tags, add analytics snippets, or reorder global assets.
When ui.preview: false, the standard mode path swaps in layout/no_preview.html instead of layout/standard.html.
The only dynamic part of the tree is inside layout/fragments/questions_panel.html: HTMX requests cause the server to render question_types/<type>.html for each visible question. Everything else follows the static include hierarchy above, so copying a layout gives you every fragment used onscreen.
The templates shown above already include the fragments with the right data. You only need to work with the values documented in Section 2; you do not have to manage the with blocks yourself unless you are creating an entirely new mode layout.
3. Widget Contract (All Question Types)¶
Every template in question_types/ receives the same inputs:
| Variable | Description |
|---|---|
question |
Question ID/path (string) |
definition |
Full questions[question] dict, including label/help/options |
value |
Current answer (None if unanswered) |
errors |
Dict of validation errors keyed by question path |
state |
session["current_state"] (NestedValue) |
handler_name |
Data handler name (e.g. "text", "bool") |
Whenever HTMX requests a widget (new question revealed, list item added, etc.) VIBE renders the same question-type template, so overriding question_types/bool.html affects both the initial page load and subsequent OOB updates.
4. HTMX & SSE Updates¶
Certain DOM regions are replaced via HTMX swaps or SSE streaming:
| Fragment | Template | Trigger |
|---|---|---|
#questions-container |
layout/fragments/questions_panel.html |
create_oob_swaps after probe |
#result-area |
layout/fragments/preview_panel.html |
Preview rerender, assistant stream |
#error-area |
layout/fragments/error_panel.html |
Validation errors / exceptions |
| Toolbar tabs/download | layout/fragments/toolbar.html |
Download readiness, assistant state |
Because the same templates power both the initial render and live updates, overriding them guarantees consistency across HTMX responses.
5. Static Assets & Icons¶
- Add
ui/theme.cssto inject template-specific styles. VIBE serves it via/interview/<template_id>/ui.cssand loads it after the built-in stylesheet. - Use standard static URLs for additional assets:
Template Icons¶
Customize the icon shown on the template selection page via ui.icon:
# config.yml
ui:
icon: "📊" # emoji
icon: "symbol:finance_mode" # Material Symbol name
icon: "icon.png" # image file from ui/ folder
icon: "images/template-icon.svg" # image with subdirectory
Icon types:
- Material Symbol (
"symbol:<name>"): Any Material Symbols icon name. VIBE already loads the font, so these render natively. Examples:"symbol:gavel","symbol:edit_note","symbol:finance_mode". - Emoji: Any emoji string (e.g.,
"🎓","📋"). - Image file: Path relative to your
ui.templatesfolder. Supported formats: PNG, JPG, SVG, GIF, WebP. Served via/interview/<template_id>/ui/<filepath>. Recommended size: 80x80px.
Detection: Values starting with symbol: are Material Symbols. Values containing / or ending with an image extension are image files. Everything else is treated as emoji.
Fallback behavior: Without ui.icon, VIBE shows the edit_document Material Symbol.
6. CSS Architecture & Design Tokens¶
VIBE uses a three-layer CSS architecture with design tokens for consistent, customizable styling:
foundation.css → style.css → theme.css (user overrides)
↓ ↓ ↓
Tokens & Application Template
Base Styles Styles Overrides
Loading Order¶
foundation.css- Design tokens (--vibe-*CSS custom properties), base typography, form styling, Bootstrap-compatible utilitiesstyle.css- VIBE application components built on foundation tokenstheme.css- Template-specific overrides (your customizations)
Design Tokens Reference¶
All tokens use the --vibe- prefix and can be overridden in your theme.css:
Core Colors¶
| Token | Default | Description |
|---|---|---|
--vibe-color-body |
#f8f6f3 |
Page background (warm off-white) |
--vibe-color-primary |
#182025 |
Primary dark (charcoal) |
--vibe-color-surface |
#ffffff |
Card/panel backgrounds |
--vibe-color-text |
#182025 |
Primary text |
--vibe-color-text-muted |
#46494a |
Secondary text |
--vibe-color-accent |
#d6b890 |
Accent highlights (tan) |
--vibe-color-light-beige |
#e1d7cc |
Input field backgrounds |
--vibe-color-border |
rgba(24,32,37,0.2) |
Standard borders |
--vibe-color-focus |
#d6b890 |
Focus state color |
Status Colors¶
| Token | Usage |
|---|---|
--vibe-color-error / --vibe-color-error-bg |
Error states |
--vibe-color-warning / --vibe-color-warning-bg |
Warning states |
--vibe-color-success / --vibe-color-success-bg |
Success states |
Typography¶
| Token | Default | Usage |
|---|---|---|
--vibe-font-sans |
Montserrat, system-ui, ... | Sans-serif stack |
--vibe-font-serif |
Spectral, Georgia, ... | Serif stack |
--vibe-font-heading |
var(--vibe-font-sans) |
Headings |
--vibe-font-body |
var(--vibe-font-serif) |
Reading content (draft/preview areas) |
--vibe-font-ui |
var(--vibe-font-sans) |
UI elements, labels |
Spacing (8px grid)¶
| Token | Value |
|---|---|
--vibe-space-1 |
4px |
--vibe-space-2 |
8px |
--vibe-space-3 |
12px |
--vibe-space-4 |
16px |
--vibe-space-6 |
24px |
--vibe-space-8 |
32px |
Other Tokens¶
| Token | Description |
|---|---|
--vibe-radius-sm/md/lg |
Border radii (1px/3px/4px) |
--vibe-shadow-sm/md/lg/xl |
Box shadows |
--vibe-transition-fast/base/slow |
Transition timings |
--vibe-z-dropdown/modal/tooltip |
Z-index scale |
Available Component Classes¶
<!-- Buttons -->
<button class="btn btn-primary">Primary</button>
<button class="btn btn-secondary">Secondary</button>
<button class="btn btn-success">Success</button>
<button class="btn btn-sm">Small</button>
<!-- Forms -->
<input type="text" class="form-control">
<div class="form-group">...</div>
<!-- Bootstrap-compatible utilities -->
<div class="mt-2 mb-4">Margins</div>
<div class="d-flex justify-content-between">Flexbox</div>
<p class="text-end">Right aligned</p>
7. Label Overrides (labels.po)¶
Template authors can override any UI text label — button text, status messages, field labels — without touching HTML or creating full template overrides.
Scaffold workflow¶
# Generate a labels.po pre-filled with all overridable strings
vibe scaffold my_template labels.po
# The file lands in your ui/ directory
# e.g. templates/my_template/ui/labels.po
The scaffold generates a standard .po file filtered to include only strings relevant to the interview UI (and any enabled extensions). Each entry includes a comment showing the current translation for easy reference:
# UI label overrides for template "my_template"
# Fill in msgstr for strings you want to override. Leave empty to keep the default.
#. Currently: "Förhandsgranskning"
#: vibe/web_core/templates/layout/fragments/toolbar.html:42
msgid "Preview"
msgstr ""
#. Currently: "Återställ"
#: vibe/web_core/templates/components/reset_button_toolbar.html:11
msgid "Reset"
msgstr ""
Editing¶
Fill in msgstr for the strings you want to change. Leave msgstr "" empty to keep the default translation.
How it works¶
When a template has a labels.po in its ui/ directory, VIBE automatically merges those overrides into the active translation catalog on each request. This affects:
- All
_()calls in templates (button labels, headings, status text) - The JavaScript translation data exposed to Alpine.js components
- Both the initial page render and HTMX partial updates
No config.yml changes are needed beyond the existing ui.templates setting — VIBE detects labels.po automatically.
Extension filtering¶
The scaffold includes only strings from the core UI paths by default. If your template uses extensions (e.g., vibe.assistant), add them to config.yml and the scaffold will include their strings too:
Locale handling¶
- For non-English locales, the scaffold adds
# Currently: "..."comments showing the existing translation - For English (
en), comments show# Default: "..."with the original English text - Overrides work for any locale, including English (useful for domain-specific terminology)
8. Example Override Workflow¶
- Generate a scaffold (optional) with
vibe scaffold <template_id> layout/standard.htmlorvibe scaffold <template_id> theme.cssto copy the stock file(s) into your template bundle. - Copy the fragment you want to customize manually if you prefer, e.g.
vibe/web_core/templates/layout/fragments/header_branding.html→<template>/ui/layout/fragments/header_branding.html. - Edit the copy:
{# ui/layout/fragments/header_branding.html #}
{% set cfg = template_config or {} %}
<h1 class="brand-heading">
{{ cfg.get("name") or template_id|replace('_', ' ')|title }}
<small>({{ _("Draft") }})</small>
</h1>
{% if cfg.get('badge_html') %}
{{ cfg.badge_html|safe }}
{% endif %}
-
Reload the interview. Because you configured
ui.templatesand mirrored the core path, VIBE automatically loads your version first. -
Optional: Override multiple fragments to build a fully branded experience. For example:
layout/fragments/questions_panel.htmlto change the form layout.question_types/text.htmlto introduce custom placeholder behavior.assistant/workbench.htmlto restyle the chat workbench.
9. Inheriting UI Overrides (ui.extends)¶
When several templates share a theme, a base template can provide ui.templates, ui.css, and labels.po while child templates extend it:
# base_brand/config.yml
ui:
templates: ui # has ui/layout/fragments/site_header.html, etc.
css: ui/theme.css
preview: false
layout: accordion
A child that does not declare ui.templates inherits the base’s entire override directory. A child that does declare its own ui.templates gets per-file merging: VIBE checks the child’s directory first, then falls back to the base’s directory for any files the child does not provide, before finally falling back to VIBE’s built-in templates.
child_with_extras/ui/
layout/fragments/header_branding.html ← child’s override
base_brand/ui/
layout/fragments/site_header.html ← inherited (child doesn’t provide this)
layout/fragments/header_branding.html ← shadowed by child’s version
In this example, the interview renders header_branding.html from the child and site_header.html from the base. Any file present in neither directory falls back to VIBE’s built-in version.
Chains are supported (grandchild → child → base). The search order is: grandchild’s ui/, then child’s ui/, then base’s ui/, then VIBE built-ins. Circular references are rejected at validation time.
10. Tips & Caveats¶
- Stick to the documented context variables; future releases will keep them stable.
- If a fragment relies on helpers (e.g.,
_,session,template_ui_stylesheet_href), they’re explicitly mentioned in the template header comment. - For large customizations, keep your overrides under version control alongside the template bundle so you can diff against new VIBE releases.
- If VIBE adds new fragments, the built-in versions still exist under
vibe/web_core/templates. Copy only the files you intend to customize.
With this model, you can treat the UI the same way you treat document templates: copy the pieces you need, edit them in your template’s ui/ folder, and rely on clear contracts for what data is available.