UI Customization Guide¶
1. What Changes for Template Authors?¶
- Dedicated override folder – Set
ui.templatesinconfig.yml(e.g.,ui.templates: ui) and place overrides under that directory. VIBE loads whatever path you configure before the built‑ins, so you can mirror only the files you want to change. - Plain-English filenames – Layout files live under
layout/, question widgets underquestion_types/, and there are no leading underscores orpartials/prefixes to decode. - Explicit include hierarchy – The interview page is composed entirely through template includes. Python no longer assembles HTML snippets, so copying a template guarantees you capture the full markup.
- Documented context contracts – Each template header lists the variables and helpers it expects. You can also consult the tables below when deciding which fragments to override.
2. 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.
3. 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.
4. 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.
5. 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.
Layout maps¶
Each diagram shows where the fragment IDs sit inside the layout so you can decide which regions to override or restyle.
6. Tabs (Contained and Line)¶
VIBE ships two tab styles that share the same base button styling as the toolbar buttons. Tabs are horizontally scrollable by default, so they keep working when you have many documents or assistants.
Contained tabs (default for toolbars)¶
Use contained tabs for document/assistant switching. Apply the tabs-shell and tabs-contained classes to the container, and tab-button to each tab. The scroll buttons are included by default and are auto-hidden when tabs fit.
<div class="tabs-shell tabs-contained" data-tabs-shell>
<button class="tab-scroll-button" type="button" data-tabs-scroll="left" aria-label="Scroll tabs left" hidden>
<span class="material-symbols" aria-hidden="true">chevron_left</span>
</button>
<div class="tabs" data-tabs-list>
<button class="tab-button active" type="button">Main Document</button>
<button class="tab-button" type="button">Appendix A</button>
<button class="tab-button" type="button">AI: Drafting</button>
</div>
<button class="tab-scroll-button" type="button" data-tabs-scroll="right" aria-label="Scroll tabs right" hidden>
<span class="material-symbols" aria-hidden="true">chevron_right</span>
</button>
</div>
Line tabs (for nav-like tab bars)¶
Use line tabs for lightweight navigation such as review page headings. Apply tabs-shell tabs-line to the container and tab-button to each link. Mark the current tab with aria-current="page" or the active class.
<nav class="tabs-shell tabs-line" data-tabs-shell>
<button class="tab-scroll-button" type="button" data-tabs-scroll="left" aria-label="Scroll tabs left" hidden>
<span class="material-symbols" aria-hidden="true">chevron_left</span>
</button>
<div class="tabs" data-tabs-list>
<a class="tab-button" href="/review/sessions" aria-current="page">Sessions</a>
<a class="tab-button" href="/review/examples">Examples</a>
<a class="tab-button" href="/review/new">New Review</a>
</div>
<button class="tab-scroll-button" type="button" data-tabs-scroll="right" aria-label="Scroll tabs right" hidden>
<span class="material-symbols" aria-hidden="true">chevron_right</span>
</button>
</nav>
Scroll behavior¶
The .tabs container uses horizontal scrolling automatically. If you need additional space for scrollbars, add padding to the container rather than to the individual buttons.
7. CSS Architecture & Design Tokens¶
VIBE uses a three-layer CSS architecture with design tokens for consistent, customizable styling:
foundation.css → style.css → ui.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 tokensui.css- Template-specific overrides (your customizations)
Design Tokens Reference¶
All tokens use the --vibe- prefix and can be overridden in your ui.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 |
Form Styling¶
VIBE uses "filled text field" styling (inspired by Carbon/Material Design):
- Light background fill (
--vibe-color-light-beige) - Bottom border only (no box)
- Border color changes on focus
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>
Customizing via ui.css¶
Create ui.css in your template's ui/ folder to override any design tokens:
/* Change accent color to blue */
:root {
--vibe-color-accent: #0066cc;
--vibe-color-focus: #0066cc;
}
/* Switch to traditional bordered inputs */
input[type="text"],
textarea,
select,
.form-control {
background-color: var(--vibe-color-white);
border: var(--vibe-border-width) solid var(--vibe-color-border);
border-radius: var(--vibe-radius-md);
}
Font Files¶
The default design uses open source fonts from Google Fonts (with system fallbacks):
- Montserrat (sans-serif) -
/static/fonts/montserrat/montserrat-latin.woff2(variable font, 400-700 weights) - Spectral (serif) -
/static/fonts/spectral/spectral-*.woff2(regular, italic, medium, semibold)
Montserrat uses the ss01 stylistic set for headings to render a single-story 'a'.
Spectral uses -0.01em letter-spacing in reading areas for tighter, more refined spacing.
If fonts aren't available, VIBE falls back to system fonts.
8. Static Assets & Icons¶
- Add
ui/theme.css(or any path set inui.css) to 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:
<link rel="stylesheet" href="{{ url_for('static', filename='fonts/my-font.css') }}">
Template Icons¶
You can customize the icon shown for your template on the template selection page by adding an icon field to your config.yml:
# config.yml
title: "My Custom Template"
description: "A template for generating reports"
# Option 1: Use an emoji
icon: "📊"
# Option 2: Use an image file from your ui folder
icon: "icon.png"
# or with a subdirectory:
icon: "images/template-icon.svg"
Icon Types:
- Emoji: Any single emoji character (e.g.,
"🎓","📋","📝") - Image file: Path relative to your
ui.templatesfolder (e.g.,"icon.png","images/logo.svg") - Supported formats: PNG, JPG, SVG, GIF, WebP
- Images are served via
/interview/<template_id>/ui/<filepath> - Recommended size: 80x80 pixels or similar square aspect ratio
Fallback behavior: If no icon is specified, VIBE selects a default emoji based on the template ID (e.g., 📝 for specs, 🎓 for tutorials, 📄 for general documents).
9. Example Override Workflow¶
- Generate a scaffold (optional) with
python app.py scaffold <template_id> layout/standard.htmlorpython app.py 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.
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.