Writing Your Own Partials

You do not register anything. Drop a .hbs file into src/_includes and it is a partial, named by its path with the extension stripped. There is no manifest to update, no config to touch, and no restart — the build globs the directory at startup and registers whatever it finds.

Your own partials are unrestricted on every plan. Only Leed’s own templates carry gates, and those are documented at Overriding Leed Templates.

Naming and nesting

The mapping is mechanical: take the path relative to src/_includes, drop the .hbs, and that string is the partial’s name.

FileInvoke as
src/_includes/header.hbs{{> header }}
src/_includes/logo/leed.hbs{{> logo/leed }}
src/_includes/testimonials/card.hbs{{> testimonials/card }}
src/_includes/svg/illustrations/pipeline.hbs{{> svg/illustrations/pipeline }}
src/_includes/leed/docs/header/chrome.hbs{{> leed/docs/header/chrome }}

Nesting is free-form and has no depth limit. Two useful consequences fall out of the mechanism:

  • A name collision is impossible. Two partials cannot share a name, because the name is the path and two files cannot share a path.
  • The leed/ prefix is not reserved by code. Nothing special-cases it. Leed’s own partials are called leed/… for the mundane reason that the build copies them into _includes/leed/, so that is what their paths are.

Passing parameters

Hash arguments on the invocation become context keys inside the partial:

{{> cards/feature title="Fast builds" icon="fa-bolt" href="/product/speed/" }}

Inside cards/feature.hbs those are {{ title }}, {{ icon }} and {{ href }}. Three details account for most of the confusion:

A partial inherits the current context. You do not need to pass everything. {{ page.url }}, {{ title }} and any global data are already visible inside the partial; hash arguments add to that context rather than replacing it.

../ reaches the parent scope. Inside an {{#each}}, this is the item and ../thing is the value from the enclosing scope. This comes up constantly when a partial is invoked from inside a loop and needs something from outside it — Leed’s own menu builder forwards four parameters that way, as ../../menuName and friends, because it is two each levels deep by the time it recurses.

A raw-HTML parameter needs triple braces. A value containing markup must be emitted with {{{ }}} or Handlebars escapes it. This is exactly what leed/menu/builder does with its onClick parameter: the caller passes an attribute string, and leed/menu/link writes it into the anchor tag with {{{ onClick }}}. If a parameter of yours renders as visible <span> on the page, this is why.

Block partials

A partial can be invoked with a body. Open it with {{#> name }}, close it with {{/name}}, and inside the partial write {{> @partial-block }} where the body should land.

The call site:

{{#> card heading="Pricing" }}
    <p>Everything on the Growth plan, plus SSO.</p>
{{/card}}

And src/_includes/card.hbs:

<article class="rounded-lg border p-6">
    <h3>{{ heading }}</h3>
{{> @partial-block }}
</article>

Parameters and a body work together — heading="Pricing" above is an ordinary hash argument.

This is not a niche trick. It is the pattern Leed sites use instead of Eleventy layout chaining: one site-template.hbs holding the whole HTML document, and every layout invoking it with its own body. That is covered at Layouts and Page Types. Leed’s own leed/docs/shell and leed/docs/header/chrome are built the same way — the shell owns <html>, <body> and the header scope, and each documentation layout supplies only its own containers as the block body.

:::warning Write {{> @partial-block }} at column 0 Handlebars indents a partial’s entire rendered output by the indentation of its call site. Indent the partial-block call and every line of the body is indented too — including the inside of <pre><code> blocks, which visibly breaks code rendering. Leed’s shipped partials carry a comment saying exactly this, in capitals, for a reason. :::

Choosing a partial at runtime

A subexpression in the partial position resolves to a partial name, so the partial you call can be a value rather than a literal:

{{> (lookup . "header") }}
{{> (docsLayoutTemplate) }}

The first is how leed/docs/shell accepts a header partial as a parameter: the layout invokes the shell with header="leed/docs/header/top-search", and the shell renders whichever name it was handed. The second is how the documentation layout picks between the alpha, bravo and charlie variants.

Reach for it when you have one wrapper and several interchangeable pieces. The alternative is an {{#if}} ladder that has to be edited every time a variant is added, and one partial name that decides everything is easier to reason about than five branches that each decide part of it.

The invocation forms

FormSyntaxUse it when
Simple{{> header }}The partial reads everything it needs from the current context
With parameters{{> cards/feature title="Fast" }}The same markup is reused with different values
Block partial{{#> card }} … {{/card}}The partial is a wrapper and the caller supplies the inside
Block partial with parameters{{#> card heading="Pricing" }} … {{/card}}A wrapper that also needs configuring — the common case for a master template
Dynamic name{{> (lookup . "header") }}One wrapper, several interchangeable variants chosen at render time

Editing partials while the server runs

A .hbs edit under _includes/ or _layouts/ triggers a full Eleventy config reset, which re-reads every partial and recompiles Tailwind. Your change appears without a restart, but the reload is heavier than a content edit and takes a beat longer. The mechanics of why are on How Templates Work, and the dev loop itself is at Local Development.

If a partial edit appears to do nothing at all — no reload, no change, no error — check the path before you check anything else. You are almost certainly editing a file under _includes/leed/, which the build overwrites from the installed site builder at the start of every run and deletes at the end of it. The complete list of what gets written into a clone that way is at What Leed Writes Into Your Repo.

Conventions worth keeping

None of these is enforced by the build. All of them come from the shipped template-patterns.md and file-structure.md references that leed site init installs into your repository, and from what shipped Leed sites actually look like.

Group by folder. A real repository’s _includes has logo/, platform/, solution/, svg/ and testimonials/ directories beside a handful of top-level files. Because the path is the name, the folder structure is the namespace — grouping costs nothing and reading {{> testimonials/card }} tells you more than {{> testimonial-card }}.

Keep files under 300 lines. A convention, not a limit; nothing in the build checks it. When a partial passes it, the thing that has usually happened is that two responsibilities ended up in one file, and splitting it also gives the second half a name.

Prefer one parameterized partial over two near-identical ones. Two files that differ by a heading and a class name will drift. One file with two hash arguments will not.

A complete worked example

src/_includes/cards/feature.hbs:

<article class="flex flex-col rounded-lg border p-6">
    {{#if icon}}
    <i class="{{ icon }} text-2xl text-(--site-primary-color)"></i>
    {{/if}}
    <h3 class="mt-4 text-lg font-semibold">{{ title }}</h3>
    <p class="mt-2 text-sm">{{ body }}</p>
    {{#if href}}
    <a href="{{ href }}" class="mt-4 text-sm font-medium">Learn more</a>
    {{/if}}
</article>

The call site, inside a page or another partial:

<div class="grid gap-6 md:grid-cols-3">
    {{> cards/feature title="Fast builds"
                      body="Incremental rebuilds in under a second."
                      icon="fa-solid fa-bolt"
                      href="/product/speed/" }}
    {{> cards/feature title="No lock-in"
                      body="Your site is a git repository you own."
                      icon="fa-solid fa-code-branch" }}
</div>

The second call omits href, so the {{#if href}} block renders nothing and that card has no link. One file, two shapes — which is the whole argument for parameters over duplication.

Everything you can call between the braces inside a partial — including the subexpressions that pick a partial name — is cataloged at Helper Index (A–Z). Leed’s own partials are named by exactly the same rule as yours, and all 54 of them are listed at Leed Partial Index.

ESC