Before you build or customize a theme, it helps to know where Bengal's responsibilities end and yours begin. The line is simple to state:
Core owns the capability — parsing, registering, and computing the render context. Your theme owns the presentation — the
*.htmltemplate and the CSS that style what core hands it.
A directive likenote or tabsis parsed and registered in core; its
visual shell (the HTML wrapper, the icon, the color) lives in a template and a
stylesheet. The same split holds for shortcodes, layouts, and partials. This
page explains how that split is enforced at render time, so you know exactly
what you inherit for free and what you must supply yourself.
Template Resolution: First Match Wins
When the engine needs a template — a layout likepage.html, a partial like
partials/header.html, or a capability template like directives/note.html—
it searches a precedence-ordered list of directories. The first directory that
contains a matching file wins.
The search order, in precedence:
- Site root
templates/— your project's top-leveltemplates/directory. Anything you put here wins over everything else. This is whatbengal theme swizzlepopulates. - The active theme chain — your theme, then its parent, then its
grandparent, walking the
extendskey in eachtheme.tomlfrom child to parent. (A child theme's chain hasdefaultfiltered out of this step, because core appends it separately in the next step.) - The bundled
defaulttheme — always appended as the final filesystem fallback, even when your theme never names it. It is added once (deduplicated against the chain above), so a project always has a complete set of layouts, partials, and capability templates to fall back to. - Library provider loaders — themes that ship a packaged component library
(declared via
librariesintheme.toml) contribute their own loaders after the filesystem search, through aChoiceLoader.
If a template is found at any step, the search stops. If even the bundled
defaulttheme (and any provider loaders) lack it, the engine raises a
TemplateNotFoundError.
The filesystem precedence is assembled in
bengal/rendering/template_engine/environment.py (resolve_template_dirs()),
where the bundled default is unconditionally appended last; the provider loaders
are layered on top inbengal/rendering/engines/kida.py (_build_loader()).
The practical takeaway: the bundleddefaulttheme is the safety net. As
long as you do not break the fallback, every layout and capability template
resolves to something.
Directives vs Shortcodes: An Asymmetry in Fallback
Both directives and shortcodes are content-authoring features that core parses and that themes can style. But they differ in one crucial way: a directive renders even when no theme template exists; a shortcode does not.
| Directives | Shortcodes | |
|---|---|---|
| Where parsed | Core, inbengal/parsing/backends/patitas/directives/ |
Core, inbengal/rendering/shortcodes.py |
| The set | Fixed — around two dozen built-in handler types (note, warning, tip, tabs, cards, dropdown, steps, …), registered in directives/registry.py |
Open — any{{ < name > }}is accepted by name |
| Extensible by | Plugins only (bengal/plugins/integration.py), not themes |
Adding a template; the name space is unbounded |
| Theme template | Optional override:templates/directives/{name}.html |
Required to render:templates/shortcodes/{name}.html |
| Missing template | Falls back to a built-in Pythonrender()— still produces HTML |
Passes the raw shortcode text through unchanged (or errors in strict mode) |
| Net result | Renders with zero theme templates | The default theme's templates are the de-facto standard library |
Why directives always render
When a directive renders, the renderer first asks the handler for a render
context (get_template_context()) and tries to render a theme template — first
directives/{name}.html (per-type, e.g. note.html), then
directives/{token_type}.html (handler-level, e.g. admonition.html). When no
template is found, that lookup returnsNoneand the renderer falls through to
the handler's ownhandler.render(...)— a Python fallback that emits HTML
directly (seebengal/parsing/backends/patitas/renderers/directives.py, the
_try_template_render() / handler.render()path).
So directives are guaranteed to produce HTML with no theme templates at all. A
theme optionally overridestemplates/directives/{name}.html(plus the
matching CSS) to change presentation, but it never has to.
Directives are a fixed set. You cannot add a new directive by dropping a
template into your theme — the handler must be registered in core, and the only
sanctioned extension point is a plugin that registers a handler through
apply_plugin_directives() in bengal/plugins/integration.py.
Why shortcodes need a template
Shortcodes have no engine-level fallback. When_render_shortcode()in
bengal/rendering/shortcodes.py looks up shortcodes/{name}.htmland the
template does not exist:
- In normal mode, it returns the raw shortcode text unchanged — the
{{ < name > }}literal leaks into the output. - In strict mode (
shortcodes.strict = truein your site config), it raises aBengalRenderingError(ErrorCode.T001) telling you to add the template or disable strict mode.
Because there is no Python fallback, whichever shortcode templates the active
theme provides define what shortcodes actually work on your site. The bundled
default theme's templates/shortcodes/directory is therefore the de-facto
standard library of shortcodes; if you extenddefault, you inherit all of
them, and if you fork away from it, any shortcode you use must be re-provided.
Implications for Theme Authors
The fallback chain and the directive/shortcode asymmetry add up to a clear rule of thumb about how much work your theme is signing up for.
A theme that extendsdefault
When yourtheme.toml declares extends = "default"(or your theme is a
site-local theme layered over the default), you inherit every capability
template — all the directive templates, all the shortcode templates — and the
default theme's component CSS at
bengal/themes/default/assets/css/components/ (for example admonitions.css,
tabs.css, dropdowns.css, steps.css, cards.css, checklist.css,
code.css). You override only the templates and styles you actually want to
change; everything else resolves through the fallback chain. This is the
low-effort path.
A theme that does not extenddefault
A theme with noextends key — such as the in-repo chirpuitheme, whose
theme.toml sets name = "chirpui"with no parent — does not inherit the
default theme's capability templates or CSS through the chain. (The bundled
default is still appended as the final filesystem fallback, but a from-scratch
theme that means to fully control presentation typically re-provides its own
templates rather than leaning on that fallback.) To deliver a complete,
self-consistent look, such a theme must re-provide every capability template
and its directive CSS. That is the structural reason a non-extending theme is
effectively a full fork: there is no Python fallback for shortcodes, and
overriding directive presentation cohesively means owning the templates and
stylesheets end to end.
The path to portable capability templates
Today, the cleanest way to get every capability template for free is to extend
default. The longer-term direction — a stable view-model contract so
capability templates can be authored portably and shared across themes without
forking the default theme — is tracked in issue #335, issue #337, and issue
#338. Those issues are the route toward capability templates that travel between
themes through a documented contract rather than by inheritance from one
specific theme.
See Also
- Create a Theme — scaffold, override, and test a theme.
- Working with Themes — use, customize, or build a theme.
- Theme Library Assets — the provider contract for packaged component libraries.