Partials are reusable HTML fragments. You define them in .html files, then include them from other templates. The compiler resolves partial references and produces one JavaScript module per HTML file, so you only load what you need at render time.
Every top-level element in a template file must have a b-name attribute. The compiler will report an error for any top-level element that lacks b-name.
You can have <html> tags, but do not include the <DOCTYPE !html>.
You point the compiler at a directory and it processes all .html files within it.
Add a b-name attribute to a top-level element. The element and its contents become the partial.
<article b-name="post">
<h1>Hello</h1>
<p>Some text.</p>
</article>If you want the partial to render only its inner content (without the container element), use <b-unwrap>:
<b-unwrap b-name="notice">
Just this text, no wrapper element.
</b-unwrap>b-name must be on a top-level element (a direct child of the implied document body). Using it on a nested element is a compilation error.
Use the b-part attribute to include a partial. The value is #partial-name for a same-file reference.
<article b-name="post">
<p>Some text...</p>
<div b-part="#notice"></div>
</article>
<p b-name="notice">
Notice!
</p>The <div> wrapper is kept in the output. To include without a wrapper, use <b-unwrap>:
<b-unwrap b-part="#notice"></b-unwrap>If the referenced partial does not exist, that is a compilation error.
Attributes on the carrying tag (including :bind/b-bind: dynamic attrs and ~ asset attrs) are rendered on the wrapping element of the partial call.
<div b-part="#notice" :class="alertCls"></div>Renders the <div> wrapper with class="..." evaluated from alertCls at render time. Falsy values (null, undefined, false) omit the attribute, matching the standard b-bind rule. Use <b-unwrap b-part="#notice"> if you do not want a wrapping element.
To make a partial available outside its own file, add b-export:
<!-- graphics/charts.html -->
<svg b-name="pie-chart" b-export>
...
</svg>Then reference it from another file using a path relative to the template root, in the form path/to/file.html#partial-name:
<!-- blog/general.html -->
<article b-name="post">
<div b-part="graphics/charts.html#pie-chart"></div>
</article>Referencing a partial in another file that does not have b-export is a compilation error.
Each HTML file compiles to one JavaScript module. Cross-file b-part references become static import statements at the top of the generated module, so the JavaScript module system handles loading — the runtime never touches file paths.
A partial can declare a slot — a place where the caller can inject content.
Default slot:
<!-- caller -->
<article b-name="post">
<div b-part="#notice">This appears in the slot.</div>
</article>
<!-- partial definition -->
<p b-name="notice">
Notice! <b-unwrap b-slot />
</p>Children of the b-part element become the default slot content. The <b-unwrap b-slot /> marker in the partial is replaced with that content at render time.
Named slots:
<!-- caller -->
<article b-name="post">
<div b-part="#notice">
<b-unwrap b-in="message">This appears in the named slot.</b-unwrap>
</div>
</article>
<!-- partial definition -->
<p b-name="notice">
Notice! <b-unwrap b-slot="message" />
</p>Use b-in="name" inside the b-part element to direct content to a named slot. Use b-slot="name" in the partial definition to declare where that slot renders.
What b-in can carry: b-in only says where the tag goes — which slot of the call it fills. What the tag is is still decided by its other directives, so any of them can ride along:
<b-unwrap b-part="#card">
<!-- a call: #chip is stamped into the "header" slot -->
<b-unwrap b-in="header" b-part="#chip"></b-unwrap>
<!-- a custom element call: same thing -->
<my-chip b-in="header"></my-chip>
<!-- a conditional: whichever branch wins renders in "header" -->
<b-unwrap b-in="header" b-if="urgent">!</b-unwrap>
<b-unwrap b-in="header" b-else>ok</b-unwrap>
<!-- a loop, carried by a real tag: every <span> lands in "header" -->
<span b-in="header" b-for="tag in tags">{{ tag }}</span>
</b-unwrap>On a regular tag (anything other than <b-unwrap>), that tag goes into the slot and whatever it carries renders inside it: <div class="w" b-in="header" b-part="#chip"></div> puts a <div class="w"> in the header slot with #chip's output inside it. b-in itself never renders as an attribute.
b-data: on the same tag binds the call's data as usual — it travels with the call, not with the slot.
Outside a call body there is nothing for b-in to route into, so there it stays a plain HTML attribute.
Slot scoping: Slot content is evaluated in the caller's data context, not the partial's. Expressions like {{ user.name }} inside slot content refer to the caller's variables.
If a slot is declared but no content is provided, the slot renders empty. If a slot does not exist in the partial, that is a compilation error.
Wrapping element: b-slot on a regular tag (anything other than <b-unwrap>) keeps that tag in the output and renders the slot content inside it. Use <b-unwrap b-slot /> to inject only the slot content with no wrapper.
<!-- partial definition -->
<div b-name="card">
<span b-slot class="body"></span>
</div>Called with slot content Hi, this renders <div><span class="body">Hi</span></div>.
Body content: anything inside a b-slot tag renders in addition to the injected content, right after it — it is not fallback content. <b-unwrap b-slot="note">(none)</b-unwrap> renders (none) whether or not the caller fills note.
Conditional slots: a flow directive on a b-slot tag wraps the insertion point, so <b-unwrap b-if="withHeader" b-slot="header"></b-unwrap> renders the header slot only when the condition holds.
A partial can hand a slot of its own on to a partial it calls, by putting b-in and b-slot on the same tag — b-slot is one more directive b-in can carry:
<!-- child: declares slot "body" -->
<div b-name="child">
<div class="inner"><b-unwrap b-slot="body" /></div>
</div>
<!-- mid: forwards its own slot "content" into child's "body" -->
<div b-name="mid">
<div b-part="#child"><b-unwrap b-in="body" b-slot="content"></b-unwrap></div>
</div>
<!-- caller: fills mid's "content" -->
<div b-name="page">
<b-unwrap b-part="#mid"><b-unwrap b-in="content">Hello</b-unwrap></b-unwrap>
</div>page renders Hello inside child's .inner, without mid having to know anything about it.
The two directives do different jobs, which is why both fit on one tag:
b-insays where the tag goes — which slot of the call it fills.b-slotsays what fills the tag — an insertion point for a slot of the enclosing partial.
Everything else about slots still applies:
- Either slot may be the default one.
<b-unwrap b-slot="content">with nob-inforwards into the callee's default slot;<b-unwrap b-in="body" b-slot>forwards the enclosing partial's default slot. - On a regular tag, that tag wraps the injected content:
<span b-in="body" b-slot="content"></span>puts a<span>in the callee'sbodyslot with the forwarded content inside it. - Content is still evaluated in the original caller's context, however many partials it passes through.
b-data:bindings along the way do not affect it. - A forwarded
b-slotdeclares that slot on its enclosing partial, so the caller'sb-inresolves against it. The same slot name may also be declared elsewhere in the partial, in which case every occurrence is filled. - Forwarding chains: a partial that receives a forwarded slot can forward it again.
Data is passed explicitly using b-data:<varname>="expression". The expression is evaluated in the caller's context.
<!-- caller -->
<article b-name="post">
<b-unwrap b-part="#notice" b-data:mood="user.mood"></b-unwrap>
</article>
<!-- partial definition -->
<p b-name="notice">
My mood is {{ mood }}.
</p>Multiple variables can be passed on the same element:
<div b-part="#card" b-data:title="item.title" b-data:count="item.count"></div>Data bindings are scoped to the partial — they are not visible outside it, and they do not override the caller's context for slot content.
Each b-data:NAME must correspond to a variable used inside the target partial. Passing b-data:NAME for a name the partial doesn't use is a compilation error (it usually means a typo or a stale binding).
The compiler reports errors for:
- A top-level element without
b-name(every top-level element must be a named partial) b-nameon a non-top-level element- A
b-partreference that cannot be resolved (partial not found) - A cross-file
b-partreference to a partial that exists but lacksb-export - A named slot reference (
b-in="name") where the partial has no matchingb-slot="name" - Default slot content provided to a partial that declares no default slot
- Circular cross-file dependencies (A includes B which includes A)
b-if,b-for,b-else, orb-else-ifon a partial definition (these are call-site directives only)b-attroutside a custom element partial definition, with a value, with an unknown modifier, or with a conflicting plain attribute on the same tag (see Declared attributes)- A required
b-attrnot provided at the call site, orb-data:NAMEcolliding with a declaredb-attr:NAME b-data:NAMEat a call site whereNAMEdoes not appear in the target partial's data shape (i.e. the partial does not use a variable of that name and does not declare ab-attr:NAME). This catches typos and stale bindings that would otherwise be silently discarded.
A hyphenated tag (an HTML custom element like <my-card>) at the top level of a file defines a partial whose name is the tag itself:
<my-notice class="notice">
Notice!
</my-notice>Calling it from another partial is just writing the tag again — no b-part, no #name:
<article b-name="post">
<p>Some text...</p>
<my-notice></my-notice>
</article>The result is a single rendered element, with attributes from the call site and the definition merged together:
<article>
<p>Some text...</p>
<my-notice class="notice">
Notice!
</my-notice>
</article>This differs from b-name/b-part partials, which always emit both the wrapping caller element and the partial's own wrapping element.
- A custom element tag must follow the HTML custom-element naming rule: a lowercase letter start, at least one hyphen, no uppercase letters.
b-*directive tags (e.g.<b-unwrap>) are not custom elements. - A hyphenated tag is treated as a partial definition only when it appears at the top level of a template file. Nested hyphenated tags are call sites.
- A custom element partial must have a closing tag, both at the definition site and at the call site. Self-closing custom elements (
<my-card />) are not valid HTML for non-void elements. - A definition cannot have
b-name,b-if,b-for,b-else, orb-else-if. - Once a custom element partial is exported with
b-export, no other definition of the same name (exported or not) may exist anywhere in the project. Two non-exported definitions of the same name in different files are allowed. - A custom element partial and a
b-namepartial cannot share the same name in the same file. - You cannot mix the two reference styles:
b-part="my-notice"does not call the custom element<my-notice>, and<part-name>does not call theb-namepartialpart-name. - Calls to unknown hyphenated tags (no matching definition anywhere) emit a warning and fall through as raw HTML — useful for browser-native custom elements that the templating system shouldn't expand.
Add b-export to a custom element definition to make it callable from any file in the project. Cross-file calls don't need a path — just write the tag:
<!-- components.html -->
<my-card class="card" b-export>
<h2>{{ title }}</h2>
<div class="body"><b-unwrap b-slot /></div>
</my-card>
<!-- page.html -->
<article b-name="post">
<my-card b-data:title="post.heading">
<p>{{ post.body }}</p>
</my-card>
</article>Slots (b-slot/b-in), data bindings (b-data:*), and attribute interpolation (:attr, b-bind:attr) all work the same as for b-name partials:
- Slot content is evaluated in the caller's context.
- The partial body and definition-side attributes are evaluated in the child context — that is, the caller's context with
b-data:*bindings overlaid. - Caller-side attributes on the call tag are evaluated in the caller's context.
This means you can mix dynamic attrs from both sides:
<my-notice :data-id="ident"></my-notice>renders as <my-notice data-id="42" class="notice">…</my-notice> when ident is 42 in the caller.
A custom element partial can declare attributes that double as context variables. On the definition tag, list each declared attribute with b-attr:NAME:
<!-- definition -->
<my-widget b-attr:label b-attr:premium.bool>
<h2>{{ label }}</h2>
<p b-if="premium">Premium content!</p>
</my-widget>Each b-attr:NAME makes NAME available as a context variable inside the partial body. The caller passes the value through a regular HTML attribute on the call tag — no b-data: needed:
<my-widget label="Hello" :premium="user.isPremium"></my-widget>The attribute is also rendered on the output tag (subject to the boolean rule below).
Without a modifier, the declared attribute is a string variable. Append .bool to declare it as a boolean:
<my-widget b-attr:label b-attr:premium.bool>...</my-widget>The modifier affects how the caller's value is coerced into the context (String(...) vs Boolean(...)) and how the rendered HTML attribute behaves: a boolean attribute is rendered as a bare NAME when truthy and omitted when falsy, mirroring the existing b-bind: boolean rule.
For b-attr:NAME (string):
| Call site | Context value | Rendered |
|---|---|---|
<my-widget> (omitted) |
— compile error | — |
<my-widget premium> (bare) |
— compile error | — |
<my-widget premium="hello"> |
"hello" |
premium="hello" |
<my-widget :premium="expr"> |
String(expr) |
premium="<value>" |
For b-attr:NAME.bool (boolean):
| Call site | Context value | Rendered |
|---|---|---|
<my-widget> (omitted) |
— compile error | — |
<my-widget premium> (bare) |
true |
bare premium |
<my-widget premium="hello"> |
true (and warning: string used where bool expected) |
premium="hello" |
<my-widget :premium="true"> |
true |
bare premium |
<my-widget :premium="false"> |
false |
omitted |
<my-widget :premium="expr"> |
Boolean(expr) |
bare premium if true, omitted if false |
:NAME and the long form b-bind:NAME behave identically.
b-attris allowed only on a custom element partial definition tag. Using it on ab-namepartial, on a call site, or on any nested element is a compile error.b-attr:NAMEcannot have a value:b-attr:NAME="x"is reserved for future use and is an error.- The only modifier currently supported is
.bool. - A declared attribute name cannot also appear as a plain attribute on the same definition tag (
<my-widget b-attr:foo foo="x">is an error). - The caller must provide every required
b-attron the call site; omitting one is an error. - Using
b-data:NAMEon the call site when the partial declaresb-attr:NAMEis an error — pass the value as an attribute instead. - Inside the partial body, a
b-attrvariable is a scalar (string or bool). Using it as an array, object, or iterable (b-for, member access, indexing) is a compile error. - A boolean
b-attrused directly in a{{ }}interpolation produces a warning. Use a stringb-attrif you need to print the value, or convert explicitly. (No warning for the reverse: a stringb-attrused in a boolean context likeb-if.) b-attr:NAMEshould be all lowercase (hyphens are fine). HTML lowercases attribute names, so a name written asb-attr:fooBaris silently treated asfoobar, and references tofooBarinside the partial body will not work. The compiler emits a warning when ab-attr:name contains uppercase letters.
A reactive custom element (one with b-attr declarations) usually pairs with a hand-coded web component on the client: a small module that imports the generated dom-patch class and calls customElements.define(...). Point the renderer at that module with b-script on the definition tag, using an asset path:
<my-widget b-attr:count b-script="@scripts/my-widget.js">
<span :data-count="count">{{ count }}</span>
</my-widget>When a page renders <my-widget>, the renderer auto-includes @scripts/my-widget.js as <script type="module"> (the entry), and <link rel="modulepreload"> for the generated dom-patch module it imports (the dependency). See JS runtime → auto-include for placement and ordering.
Rules:
b-scriptis allowed only on a custom element partial definition tag. Using it elsewhere is a compile error.- Its value is an asset path (
@name/subpath); the asset directory must be configured (see Assets) and the file must exist. At most oneb-scriptper definition. - A reactive partial with generated dom-patch code but no
b-scriptbuilds with a warning — nothing would register its component.
If the same attribute name appears on both the call site and the definition (e.g. both set class), the compiler reports an error. Special handling for class merging is not yet implemented. Names declared via b-attr:NAME are exempt from this check — that's the whole point of b-attr.
b-for, b-if, b-else-if, and b-else can be placed directly on a call tag — the call (and its slot content) is wrapped in the matching loop or branch. This works on a custom element call:
<my-greeting b-for="who in names" b-data:name="who"></my-greeting>
<my-notice b-if="warn"></my-notice>
<my-banner b-else-if="info"></my-banner>
<my-banner b-else b-data:tone="'quiet'"></my-banner>and on a b-part call, on b-unwrap or on a wrapper element (where the wrapper is inside the loop or branch too):
<b-unwrap b-if="warn" b-part="#notice"></b-unwrap>
<div class="row" b-for="who in names" b-part="#greeting" b-data:name="who"></div>The b-for variable (who above) is in scope for b-data:* bindings and slot content on the same call. b-else-if/b-else chain to a preceding b-if among siblings just like they do on regular tags, and the preceding b-if can be on any of these.
The equivalent <b-unwrap b-for=...> wrapping form is also supported and produces the same output — use whichever reads better in context:
<b-unwrap b-for="who in names">
<my-greeting b-data:name="who"></my-greeting>
</b-unwrap>