Customize Bengal themes without breaking theme updates. Use theme inheritance, template overrides, and CSS customization techniques.
Understand Theme Resolution
Bengal resolves themes in this order:
- Project themes -
themes/your-theme/(highest priority) - Installed themes - Installed via pip/uv
- Bundled themes - Built into Bengal (e.g.,
default)
Check Active Theme
# List available themes
bengal utils theme list
# Get theme info
bengal utils theme info default
# Debug theme resolution
bengal utils theme debug
Create a Project Theme
Option 1: Start from Scratch
bengal new theme my-custom-theme
This creates:
themes/my-custom-theme/
├── theme.yaml
├── templates/
│ ├── base.html
│ ├── page.html
│ └── partials/
│ ├── header.html
│ └── footer.html
└── static/
├── css/
│ └── style.css
└── js/
└── main.js
Configure Your Theme
Editbengal.toml:
[theme]
name = "my-custom-theme"
default_appearance = "light"
Override Templates Selectively
Template Inheritance
You don't need to copy all templates. Override only what you need:
themes/my-custom-theme/templates/base.html:
{# Extend default theme's base template #}
{% extends "default/base.html" %}
{# Override only the header block #}
{% block header %}
<header class="custom-header">
<h1>{{ site.title }}</h1>
<nav>
{% for item in menu.main %}
<a href="{{ item.href }}">{{ item.name }}</a>
{% end %}
</nav>
</header>
{% end %}
{# Everything else inherits from default theme #}
Partial Overrides
Override specific partials:
themes/my-custom-theme/templates/partials/footer.html:
<footer class="custom-footer">
<p>© {{ site.author }} {{ "now" | date("%Y") }}</p>
<p>Custom footer content</p>
</footer>
Bengal will use your partial instead of the theme's default.
Page Hero Templates
The page hero templates display the header section of content pages including title, breadcrumbs, description, and stats.
Template Structure
Thepage-hero/directory contains separated hero templates:
templates/partials/
├── page-hero.html # Dispatcher (routes by hero_style)
└── page-hero/ # Separated hero templates
├── _macros.html # Shared macros and helpers
├── index.html # Index page hero template
├── element.html # DocElement pages (modules, classes, commands)
└── section.html # Section-index pages (packages, CLI groups)
The dispatcher (page-hero.html) routes to the appropriate template based on page type and hero_styleconfiguration.
Override Page Hero for API Pages
To customize the hero for API documentation pages:
For element pages (modules, classes, functions, commands):
Overridepartials/page-hero/element.htmlto customize the hero for API documentation elements:
{# themes/my-theme/templates/partials/page-hero/element.html #}
{% from 'partials/page-hero/_macros.html' import hero_element %}
{# Use the macro with custom parameters #}
{{ hero_element(element, params, theme) }}
{# Or create a completely custom hero #}
<div class="page-hero">
<div class="page-hero__badges">
{% include 'autodoc/python/partials/badges.html' %}
</div>
<h1 class="page-hero__title page-hero__title--code">
<code>{{ element.qualified_name }}</code>
</h1>
{% if element.description %}
<div class="page-hero__description">
{{ element.description | markdownify | safe }}
</div>
{% end %}
</div>
For section-index pages:
Overridepartials/page-hero/section.htmlto customize section index heroes:
{# themes/my-theme/templates/partials/page-hero/section.html #}
{% from 'partials/page-hero/_macros.html' import hero_section %}
{# Use the macro #}
{{ hero_section(section, params, theme) }}
{# Or create a custom section hero #}
<div class="page-hero">
<h1 class="page-hero__title">{{ section.title }}</h1>
{% let desc = section.metadata.description %}
{% if desc %}
<div class="page-hero__description">
{{ desc | markdownify | safe }}
</div>
{% end %}
</div>
Using hero_context
For CLI reference sections, pass explicit context to avoid URL sniffing:
{# In autodoc/cli/section-index.html #}
{% let hero_context = {'is_cli': true} %}
{% include 'partials/page-hero/section.html' %}
The hero_context.is_cliflag controls whether stats display:
true: "X Groups, Y Commands"false: "X Packages, Y Modules"
Template Data Access Patterns
Element templates receive aDocElementdataclass—use attribute access:
element.qualified_nameelement.descriptionelement.childrenelement.source_file
Section templates receive aSectionobject—use attribute access:
section.title(section title)section.metadata.description(safe access, returns empty string if missing)section.sorted_pages(sorted child pages)section.sorted_subsections(sorted child sections)
Customize CSS
Method 1: Override Theme CSS
Create your own CSS file that overrides theme styles:
themes/my-custom-theme/static/css/custom.css:
/* Override theme colors */
:root {
--color-primary: #3498db;
--color-text: #2c3e50;
}
/* Custom styles */
.custom-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
padding: 2rem;
}
Include in your base template:
themes/my-custom-theme/templates/base.html:
{% extends "default/base.html" %}
{% block extra_head %}
<link rel="stylesheet" href="{{ asset_url('css/custom.css') }}">
{% end %}
Method 2: Use CSS Variables
Many themes support CSS variables. Override them:
themes/my-custom-theme/static/css/overrides.css:
:root {
/* Override default theme variables */
--theme-primary: #3498db;
--theme-secondary: #2ecc71;
--theme-font-sans: 'Inter', sans-serif;
}
Theme Configuration Options
Themes can expose configuration options in two ways:
Theme-Level Configuration (theme.yaml)
Define default configuration in your theme'stheme.yamlfile:
themes/my-custom-theme/theme.yaml:
name: my-custom-theme
version: 1.0.0
# Custom configuration options
show_author: true
show_date: true
sidebar_position: left
color_scheme: light
Site-Level Overrides (bengal.toml)
Override theme defaults in your site'sbengal.toml:
[theme]
name = "my-custom-theme"
# Override theme config values
show_author = true
sidebar_position = "right"
color_scheme = "dark"
Accessing Configuration in Templates
Access theme configuration usingtheme.get()or direct property access:
{# Using theme.get() method (recommended) #}
{% if theme.get('show_author', false) %}
<p>By {{ page.author or site.author }}</p>
{% end %}
{# Direct config key access #}
{% if theme.config.show_author %}
<p>By {{ page.author or site.author }}</p>
{% end %}
Note: All keys in the [theme] section are accessible via theme.get('key') (with default) or theme.config.key(direct access).
Best Practices
Don't Modify Installed Themes
❌ Bad:
# Don't edit installed theme directly
vim $(python -m site --user-site)/bengal/themes/default/templates/base.html
✅ Good:
# Create project theme that extends default
bengal new theme my-theme
# Override only what you need
Use Theme Inheritance
✅ Good:
{% extends "default/base.html" %}
{% block header %}
{# Only override header #}
{% end %}
❌ Bad:
{# Copying entire base.html #}
<!DOCTYPE html>
<html>
{# ... hundreds of lines ... #}
</html>
Troubleshooting
Theme Not Found
Issue:Theme 'my-theme' not found
Solutions:
- Verify theme directory exists:
themes/my-theme/ - Check
theme.tomlhas correctnamefield - Run
bengal utils theme listto see available themes
Template Inheritance Not Working
Issue: Changes to parent theme not reflected
Solutions:
- Verify
extendspath is correct:"default/base.html" - Check theme chain:
bengal utils theme debug - Clear cache:
bengal clean --cache
CSS Not Loading
Issue: Custom CSS not applying
Solutions:
- Use
asset_url()filter:{{ asset_url('css/style.css') }} - Check file location:
themes/your-theme/static/css/ - Hard refresh:
Cmd+Shift+R
Navigation with NavTree
Bengal provides a pre-computed navigation tree for efficient template rendering. Useget_nav_tree(page)to access the navigation structure.
Basic Usage
<nav class="sidebar">
{% for item in get_nav_tree(page) %}
<a href="{{ item.href }}"
{% if item.is_current %}class="active"{% end %}
{% if item.is_in_trail %}class="in-trail"{% end %}>
{{ item.title }}
</a>
{% if item.has_children %}
<ul>
{% for child in item.children %}
<li>
<a href="{{ child.href }}">{{ child.title }}</a>
</li>
{% end %}
</ul>
{% end %}
{% end %}
</nav>
NavNode Properties
Each navigation node provides:
| Property | Type | Description |
|---|---|---|
item.title |
str |
Display title |
item.href |
str |
Page URL with baseurl applied |
item._path |
str |
Site-relative URL without baseurl |
item.icon |
str | None |
Icon identifier |
item.weight |
int |
Sort weight |
item.children |
list[NavNode] |
Child navigation items |
item.is_current |
bool |
True if this is the current page |
item.is_in_trail |
bool |
True if in active trail to current page |
item.is_expanded |
bool |
True if node should be expanded |
item.has_children |
bool |
True if node has children |
item.depth |
int |
Nesting level (0 = top level) |
Scoped Navigation
For section-specific navigation (e.g., docs-only sidebar):
{% let root = page._section.root if page._section else none %}
{% for item in get_nav_tree(page, root_section=root) %}
<a href="{{ item.href }}">{{ item.title }}</a>
{% end %}
Benefits
- Performance: O(1) lookup via cached structure (<1ms render overhead)
- Simplicity: Single function call replaces version-filtering boilerplate
- Consistency: Pre-computed structure ensures consistent navigation across pages
- Version-aware: Automatic version filtering and shared content injection
Seealso
- Templating — Template basics
- Assets — Asset pipeline
- Icon Reference — SVG icons and customization
- Variables Reference — Available template variables