Skip to content

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.css to inject template-specific styles. VIBE serves it via /interview/<template_id>/ui.css and 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

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.templates folder. 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

  1. foundation.css - Design tokens (--vibe-* CSS custom properties), base typography, form styling, Bootstrap-compatible utilities
  2. style.css - VIBE application components built on foundation tokens
  3. theme.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.

msgid "Preview"
msgstr "Visa utkast"

msgid "Reset"
msgstr "Börja om"

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:

# config.yml
extensions:
  - vibe.assistant

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

  1. Generate a scaffold (optional) with vibe scaffold <template_id> layout/standard.html or vibe scaffold <template_id> theme.css to copy the stock file(s) into your template bundle.
  2. 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.
  3. 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 %}
  1. Reload the interview. Because you configured ui.templates and mirrored the core path, VIBE automatically loads your version first.

  2. Optional: Override multiple fragments to build a fully branded experience. For example:

    • layout/fragments/questions_panel.html to change the form layout.
    • question_types/text.html to introduce custom placeholder behavior.
    • assistant/workbench.html to 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
# child_template/config.yml
ui:
  extends: base_brand    # inherits everything above

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/config.yml
ui:
  extends: base_brand
  templates: ui          # child’s own ui/ dir
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.