Custom and Overriding Blocks
Block templates are HTML files that render :::{.blockname} fenced divs. They live in blocks/ inside your site root, your theme directory, or (for built-ins) embedded in the braised binary.
A complete block definition has two parts — the template and the schema. The template controls how the block renders. The schema describes its interface for braised inspect and AI tools. Both live in the same .html file, but they are independent: a block with only a template renders correctly, and a block with only schema markers never renders at all. You need both.
Resolution order
When braised encounters :::{.callout}, it searches in this order:
{siteDir}/blocks/callout.html— site-local override (highest priority){themeDir}/blocks/callout.html— theme-provided block- Built-in embedded
callout.html— braised default
The first match wins. No braised.yaml declaration is needed — the presence of the file activates the block.
Writing a custom block
Create blocks/mycard.html in your project root (or theme). The file contains both the template HTML and the schema markers:
{{/* braised:description: A content card with an optional title and variant style. */}}
{{/* braised:prop: title type=string default="" description="Card heading text." */}}
{{/* braised:prop: variant type=enum values=default,primary,warning default=default description="Visual style." */}}
<div class="card card--{{index .Props "variant" | default "default"}}">
{{if index .Props "title"}}
<h3 class="card-title">{{index .Props "title"}}</h3>
{{end}}
<div class="card-body">{{.Children}}</div>
</div>
Schema markers (lines 1–3)
Go template comments beginning with braised: are read by the scanner at startup. They are not rendered — they describe the block's interface for braised inspect and get_block_list.
{{/* braised:description: A content card with an optional title and variant style. */}}
{{/* braised:prop: title type=string default="" description="Card heading text." */}}
{{/* braised:prop: variant type=enum values=default,primary,warning default=default description="Visual style." */}}
Each braised:prop: declares one accepted prop: name, type, allowed values (for enums), default, and description. A prop with no default= is required — default="" makes title optional here. See Block Schema Declaration for the full marker reference.
Template HTML (lines 4–10)
The template receives props and children from the page and produces HTML. The scanner ignores this section entirely.
<div class="card card--{{index .Props "variant" | default "default"}}">
{{if index .Props "title"}}
<h3 class="card-title">{{index .Props "title"}}</h3>
{{end}}
<div class="card-body">{{.Children}}</div>
</div>
index .Props "key" reads a prop value. | default "fallback" substitutes a value when the prop is absent. {{.Children}} renders inner Markdown content.
Use the block in Markdown:
:::{.mycard title="Getting started" variant="primary"}
Follow these steps to install the CLI.
:::
Template context
The template context (.) is a BlockContext:
.Props map[string]string — attributes from the opening fence
.Children template.HTML — rendered inner content
.ChildBlocks []ChildContext — direct child blocks (name, props, rendered)
.Page.Title string — current page title
.Page.URL string — current page URL
.Page.Breadcrumb []string — nav trail labels (outermost → current page)
.Page.Headings []Heading — H2/H3 headings on the current page
.Page.Meta map[string]string — page-level frontmatter extra keys
.Site.Title string — site name
.Site.URL string — site base URL
.Site.Meta map[string]any — site-level meta from braised.yaml
.RegisterRef func — register a cross-reference target (see below)
RegisterRef
Block templates can register named cross-reference targets so other pages can link to content they generate:
{{call .RegisterRef "api:create-user" "POST /users" "create-user"}}
Arguments: id (unique site-wide ref ID), title (link text when unspecified by the author), anchorID (in-page fragment — use "" to target the page root).
The registered ID is then reachable from any page via braised:ref/api:create-user.
Structural blocks
A structural block may contain other blocks as direct children. It requires a named close (:::{/blockname}) rather than a plain :::. Declare it with a braised:structural marker at the top of the template:
{{/* braised:structural */}}
{{/* braised:description: A layout container for card child blocks. */}}
{{/* braised:children: card */}}
<div class="card-grid">
{{range .ChildBlocks}}{{.Rendered}}{{end}}
</div>
braised:structural is a schema marker like any other — it is scanned at startup and sets the structural flag. No braised.yaml entry is needed.
The alternative is to declare structural blocks in braised.yaml. Use this when the template is provided by a third party and you cannot edit it:
blocks:
structural: [mycontainer, myrow]
Both approaches may be combined. Built-in structural blocks (spotlight, tabs, tab, code-group, and others) are always structural without any declaration.
Overriding a built-in block
To change how callout renders, copy it to blocks/callout.html and edit the HTML. The built-in source is at internal/block/builtin/ in the braised repository.
:::callout{type="warning" title="Overriding the template does not carry over the schema"}
When blocks/callout.html exists in your project, braised scans that file for braised: markers. The built-in's schema (description, props, structural flag) is no longer used — it is replaced by whatever your file declares.
If your override has no markers, braised inspect shows schema=undecorated and get_block_list returns the block with no description or props. AI agents lose all knowledge of the block's interface.
Re-declare the schema in your copy. If you are making a purely visual change with the same props as the original, the built-in's marker declarations are a safe starting point:
{{/* braised:description: Highlights important information. */}}
{{/* braised:prop: type type=enum values=note,tip,warning,caution default=note description="Visual severity." */}}
{{/* braised:prop: title type=string default="" description="Optional heading." */}}
{{/* your modified HTML below */}}
If your override uses different props, write markers that match your template, not the original.
Block CSS conventions
Built-in block styles live in braised:theme/base. Two conventions apply when writing block CSS — either for a custom block or when overriding a built-in:
Blocks do not set max-width. The content column (article { max-width: var(--content-max) }) owns layout constraints. Blocks that need to expand beyond the normal column padding use negative margin-inline and let the column cap the overall width. This keeps ultrawide behavior consistent across all blocks without any per-block width logic.
Built-in blocks expose --blockname-* custom properties as theming handles. Override them after @import "braised:theme/base" — no template copy required:
@import "braised:theme/base";
/* Widen the pull-quote rule color to match a brand accent */
.pull-quote {
--pull-quote-border: #e05c2a;
}
/* Reduce spotlight expansion on level 2 */
.spotlight[data-level="2"] {
--spotlight-bleed: 1rem;
}
When writing a custom block, follow the same pattern — declare tuneable values as custom properties on the block's root element with sensible defaults:
.my-card {
--my-card-radius: 8px;
--my-card-padding: 1.25rem;
border-radius: var(--my-card-radius);
padding: var(--my-card-padding);
background: var(--bg-surface);
border: 1px solid var(--border);
}
Block JavaScript
If a block requires client-side behaviour, place a .js file alongside the template:
blocks/
accordion.html
accordion.js ← included in the blocks.js bundle automatically
The JS file is concatenated into dist/assets/blocks.js in priority order: built-in → theme → site root. A site-local blocks/accordion.js can override the theme's version of the same file, exactly like HTML templates.
Register components with Alpine:
document.addEventListener('alpine:init', () => {
Alpine.data('accordion', () => ({
open: false,
toggle() { this.open = !this.open; }
}));
});