UI Customization Guide


1. What Changes for Template Authors?

  1. Dedicated override folder – Set ui.templates in config.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.
  2. Plain-English filenames – Layout files live under layout/, question widgets under question_types/, and there are no leading underscores or partials/ prefixes to decode.
  3. 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.
  4. 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

Standard layout (two-column)
layout/standard.html #config-area layout/fragments/ questions_panel.html → #questions-container layout/fragments/toolbar.html #error-area #result-area
No-preview layout (single column)
layout/no_preview.html layout/fragments/header_branding.html layout/fragments/questions_panel.html → #questions-container #error-area layout/fragments/toolbar.html
Assistant layout (Workbench)
assistant/workbench.html Conversation history assistant/user_bubble.html assistant/assistant_bubble.html assistant/assistant_form.html layout/fragments/toolbar.html layout/fragments/error_panel.html Live draft area (#result-area)

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

  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. ui.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 in ui.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

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

  1. Generate a scaffold (optional) with python app.py scaffold <template_id> layout/standard.html or python app.py 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:

  3. layout/fragments/questions_panel.html to change the form layout.
  4. question_types/text.html to introduce custom placeholder behavior.
  5. assistant/workbench.html to 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.