Element Partials
How to use entry.render() and the _partials/ template directory in Craft CMS 5 for reusable element rendering. Element partials are a front-end feature — the CP does not use them. For CP element display (chips, cards, table rows), see the craftcms skill's element-index.md.
Documentation
- Element partials: https://craftcms.com/docs/5.x/system/elements.html#rendering-elements
Common Pitfalls
- Forgetting to create the partial template —
render()silently falls back to a<p>tag with the element's title. No error, no warning — just a plain text title where your card should be. - N+1 queries inside partials — use
.eagerly()for any related element queries. Each partial renders independently, so without eager loading, every card in a grid fires separate queries. - Assuming
render()works in the CP — it's front-end only (TEMPLATE_MODE_SITE). - Hardcoding the partial path instead of using the convention — the lookup path is automatic. Don't
{% include %}the partial manually whenentry.render()works. - Overriding an entry type's handle per field and expecting the partial to follow — it does, but only on Craft 5.10.14+. On older versions the override is ignored for lookup, so the partial must live under the original handle.
Contents
- How It Works
- Template Lookup Path
- Available Variables
- Passing Custom Variables
- Common Patterns
- Configuration
How It Works
Every element in Craft 5 has a render() method that looks for a matching template in _partials/, renders it, and returns the HTML as a Twig\Markup object (safe for direct output).
{# Render a single entry #}
{{ entry.render() }}
{# Render a list of entries #}
{% for entry in craft.entries().section('blog').limit(10).all() %}
{{ entry.render() }}
{% endfor %}
{# Render with custom variables #}
{{ entry.render({ size: 'compact', showExcerpt: false }) }}If no matching template exists, render() falls back to a <p> tag containing the element's title — no error is thrown.
Template Lookup Path
render() builds a list of candidate templates and renders the first one that exists (lowest priority number wins). Since 5.8.0 plugins can add or reorder candidates through Element::EVENT_RENDER.
Priority 1: Overridden entry type handle (5.10.14+)
_partials/{refHandle}/{overriddenHandle}.twigEntries only, and only when the entry type's handle is overridden for the field or section it's used in (per-usage overrides, 5.6.0+). Before 5.10.14 this candidate didn't exist: a cta type renamed to richTextCta inside a CKEditor field still rendered _partials/entry/cta.twig (craftcms/cms#18968).
Priority 2: Type-specific partial
_partials/{refHandle}/{providerHandle}.twig{refHandle}— the element's reference handle:entry,asset,user,address{providerHandle}— the field layout provider's handle: for entries, this is the original entry type handle
Examples:
_partials/entry/article.twig ← article entry type
_partials/entry/blogPost.twig ← blogPost entry type
_partials/asset/image.twig ← (if assets had type-specific layouts)
_partials/user.twig ← user fallback (no type handle)Priority 3: Generic fallback
_partials/{refHandle}.twigExamples:
_partials/entry.twig ← fallback for all entry types
_partials/asset.twig ← fallback for all assetsThe most specific existing template wins. The generic fallback catches everything else.
Available Variables
Inside the partial template, the element is available under its reference handle:
| Element Type | Variable Name |
|---|---|
| Entry | entry |
| Asset | asset |
| User | user |
| Address | address |
| Custom element | The element's refHandle() return value |
For nested entries (Matrix, CKEditor), the entry variable has owner and field properties for accessing the parent context.
Passing Custom Variables
Pass an associative array to render(). The variables merge into the template context:
{{ entry.render({ size: 'hero', showMeta: true, imagePreset: 'wide' }) }}In the partial, guard with defaults:
{# _partials/entry/article.twig #}
{% set size = size ?? 'default' %}
{% set showMeta = showMeta ?? true %}
{% set imagePreset = imagePreset ?? 'card' %}
<article class="article article--{{ size }}">
{% set image = entry.featuredImage.eagerly().one() %}
{% if image %}
{{ image.render({ preset: imagePreset }) }}
{% endif %}
<h3><a href="{{ entry.url }}">{{ entry.title }}</a></h3>
{% if showMeta %}
<time datetime="{{ entry.postDate | atom }}">
{{ entry.postDate | date('M j, Y') }}
</time>
{% endif %}
</article>Common Patterns
Blog card grid
{# templates/blog/index.twig #}
{% set entries = craft.entries()
.section('blog')
.with(['featuredImage', 'author', 'topics'])
.limit(12)
.all() %}
<div class="grid grid-cols-3 gap-6">
{% for entry in entries %}
{{ entry.render({ size: 'card' }) }}
{% endfor %}
</div>Varying layout by entry type
The type-specific lookup means different entry types render differently without conditionals:
_partials/entry/article.twig ← article layout (image + excerpt)
_partials/entry/video.twig ← video layout (embed + duration)
_partials/entry/event.twig ← event layout (date + venue){# All entries render with their own partial — no if/switch needed #}
{% for entry in entries %}
{{ entry.render() }}
{% endfor %}Nested element rendering
Matrix entries and CKEditor nested entries also support render():
{# Render Matrix blocks #}
{% for block in entry.contentBlocks.all() %}
{{ block.render() }}
{% endfor %}Each block type gets its own partial: _partials/entry/heroBlock.twig, _partials/entry/textBlock.twig, etc.
Eager loading inside partials
Use .eagerly() on custom relation fields to avoid N+1 queries when rendering a list:
{# _partials/entry/article.twig #}
{% set image = entry.featuredImage.eagerly().one() %} {# custom Assets field #}
{% set categories = entry.topics.eagerly().all() %} {# custom relation field #}
{% set author = entry.author %} {# native attribute — access directly #}.eagerly() batches queries across all partials in the same render cycle.
Native attributes can't be .eagerly()-loaded. entry.author (also entry.authors, an asset's uploader, a user's photo) returns the element directly — not a query — so there's no .eagerly()/.one() to chain; entry.author.eagerly() throws. Cover them with .with(['author']) on the outer query instead, as the Blog card grid above does. See craft-content-modeling → relations-and-eager-loading.md for the full .with()-vs-.eagerly() rules.
Asset partials
{# _partials/asset.twig #}
{% set preset = preset ?? 'default' %}
{% if asset.kind == 'image' %}
<img src="{{ asset.url }}"
width="{{ asset.width }}"
height="{{ asset.height }}"
alt="{{ asset.alt ?? asset.title }}"
loading="lazy">
{% else %}
<a href="{{ asset.url }}">{{ asset.title }}</a>
{% endif %}Configuration
| Setting | Default | Purpose |
|---|---|---|
partialTemplatesPath |
'_partials' |
Base directory within templates/ for partial lookup |
// config/general.php
->partialTemplatesPath('_components/partials')Or via environment variable: CRAFT_PARTIAL_TEMPLATES_PATH.