Twig Render Reference
The render adapter generates complete HTML output for images, galleries, pagination, and other UI components. For URL-only access, see cms.media.
Images
Section titled “Images”Image functions take three separate arguments: the object, image transform options, and collection/render context. These are separate objects — do not merge them.
cms.render.image(object, {transforms}, {context}) │ │ │ │ │ └── collection, property, loading │ └── w, h, fit, fm, etc. └── object ID or full objectimage()
Section titled “image()”Render a complete <img> tag with ImageWorks URL, dimensions, alt text, and lazy loading.
{# Basic image #}{{ cms.render.image('hero') }}
{# With ImageWorks transformations #}{{ cms.render.image('hero', {w: 800, h: 600, fit: 'crop'}) }}
{# Custom collection and property — note: separate argument from transforms #}{{ cms.render.image('widget', {}, {collection: 'products', property: 'photo'}) }}
{# Pass object directly (recommended) #}{{ cms.render.image(product, {w: 600}, {collection: 'products', property: 'image'}) }}
{# Eager loading (default is lazy) #}{{ cms.render.image('hero', {}, {loading: 'eager'}) }}| Parameter | Type | Default | Description |
|---|---|---|---|
idOrObject | string|array|null | required | Object ID or full object data |
imageworks | array | [] | ImageWorks transformation parameters (w, h, fit, fm, etc.) |
options | array | [] | Collection context: collection, property, loading |
Nested images (cards and decks)
Section titled “Nested images (cards and decks)”Images stored inside a card or deck field are addressed through the property option using a dot-notation path:
{# Card child — first segment is the card field, last segment is the child key #}{{ cms.render.image('post-1', {w: 800}, {property: 'mycard.image'}) }}
{# Deck child — first segment is the deck field, then the deck-item id, then the child key #}{{ cms.render.image('post-1', {w: 800}, {property: 'mydeck.item-3.image'}) }}cms.render.alt() accepts the same property: 'parent.child' syntax. See cms.media → Nested images for the underlying URL convention.
picture()
Section titled “picture()”Render a responsive <picture>: one <source> per modern format, each carrying a srcset of ImageWorks candidates, then an <img> fallback in the image’s own format. The fallback carries the same srcset, so a browser that ignores <picture> still picks a sensible size.
Same three arguments as image(): the object, then ImageWorks transforms, then collection/render context. The transforms are applied to every candidate in every srcset — the only thing that varies between candidates is w, which picture() sets itself.
{# Defaults: 480/768/1024/1440/1920 candidates, AVIF + WebP sources, sizes="100vw" #}{{ cms.render.picture('hero') }}
{# An image that never spans the viewport — tell the browser, or it downloads for 100vw #}{{ cms.render.picture(post, {}, {sizes: '(min-width: 60em) 50vw, 100vw', collection: 'blog'}) }}
{# Your own candidate widths and a single format #}{{ cms.render.picture(post, {}, {widths: [400, 800, 1200], formats: ['webp']}) }}
{# The transforms apply to every candidate; w there is a ceiling, not a candidate #}{{ cms.render.picture(post, {w: 1200, h: 675, fit: 'crop-focalpoint', q: 75}) }}| Parameter | Type | Default | Description |
|---|---|---|---|
idOrObject | string|array|null | required | Object ID or full object data |
imageworks | array | [] | ImageWorks transforms applied to every candidate — h, fit, q, a preset p, and so on. Two keys behave differently from image(): w is a ceiling on the largest candidate rather than a candidate itself, and fm is ignored, because each <source> sets its own format from the formats option |
options | array | [] | The picture options below, plus the same context as image(): collection (default 'image'), property (default 'image', dotted for card/deck-nested), loading (default 'lazy') and class |
| Option | Type | Default | Description |
|---|---|---|---|
widths | int[] | [480, 768, 1024, 1440, 1920] | Candidate widths for every srcset. Any wider than the source are dropped and the source width joins as the largest — ImageWorks never upscales, so a wider candidate would deliver the source’s own width and mis-report it |
formats | string[] | ['avif', 'webp'] | One <source> per format, in order, best first. A format equal to the image’s own is skipped as redundant. Any of jpg, png, webp, avif |
sizes | string | '100vw' | The sizes attribute on every <source> and the <img>. 100vw is what a browser assumes without it, so narrow it per call |
collection, property, loading, class | as image() | Collection context, dotted nested property, lazy loading, extra class |
Every w descriptor is the width ImageWorks will actually deliver for that candidate, after h and fit are applied — never the requested number. Two requested widths that clamp to one delivered width collapse into one candidate. A GIF gets no <source> children at all, since re-encoding to a still format would drop its animation. Change the defaults site-wide under imageworks.picture in config/tcms.php; see Format & Quality for the format list.
Get the alt text for an image. Falls back through alt text, EXIF data, then filename.
{{ cms.render.alt('hero') }}{{ cms.render.alt(product, {collection: 'products', property: 'image'}) }}
{# Card child #}{{ cms.render.alt('post-1', {property: 'mycard.image'}) }}cms.render.video()
Section titled “cms.render.video()”Render a video field property (or a local file-field video upload) as an
embed, a <video> element, or a click-to-play facade — whichever fits the
provider.
{# An object from the ready-made video collection: collection and property both default to "video". Hosted providers render as a click-to-play facade (poster + play button) by default. #}{{ cms.render.video('intro') }}
{# Hosted provider (YouTube, Vimeo, Livid, Bunny, Cloudflare, Loom, Wistia, Publitio, Jet-Stream) on your own schema #}{{ cms.render.video(post, {property: 'promo'}) }}
{# An eager iframe instead of the facade #}{{ cms.render.video(post, {property: 'promo', facade: false}) }}
{# Direct file URL (the `file` provider) — a muted looping background clip #}{{ cms.render.video(post, {property: 'trailer', autoplay: true, muted: true, loop: true}) }}
{# Facade options carry into the embed built on click #}{{ cms.render.video(post, {property: 'promo', muted: true}) }}
{# The uploaded poster resized through ImageWorks — the trailing argument, since video options lead in a video API #}{{ cms.render.video(post, {property: 'promo'}, {w: 1200}) }}
{# A file-field value (mime starting video/) streams through the same call #}{{ cms.render.video(post, {property: 'localClip'}) }}| Option | Type | Default | Description |
|---|---|---|---|
collection | string | 'video' | Collection identifier |
property | string | 'video' | Property name |
autoplay | bool | false | Autoplay (subject to browser muted-autoplay rules) |
loop | bool | false | Loop playback |
muted | bool | false | Mute |
controls | bool | true | Show player controls (file provider only) |
class | string | '' | Extra CSS class on the wrapper |
poster | string | '' | Override poster URL — otherwise resolved via cms.media.videoPoster() |
facade | bool | true | Click-to-play poster with a play button; the iframe loads on click. Set false for an eager iframe. Ignored for the file provider, and for any value where no poster or thumbnail resolves, which renders the eager iframe instead (an unknown URL has no vendor thumbnail, so the poster must be uploaded) |
An unknown provider URL is never modified — it renders as a generic iframe
with the author’s URL exactly as pasted. See Video for
the full provider table and field settings.
Galleries
Section titled “Galleries”gallery()
Section titled “gallery()”Render a complete gallery grid with LightGallery lightbox support.
{# Default gallery (300x200 thumbnails) #}{{ cms.render.gallery('vacation') }}
{# Custom thumbnail size #}{{ cms.render.gallery('vacation', {w: 150, h: 150}) }}
{# Thumbnail and full-size settings #}{{ cms.render.gallery('vacation', {w: 150}, {w: 1200}) }}
{# With options #}{{ cms.render.gallery('vacation', {w: 200}, {}, { maxVisible: 8, viewAllText: 'Show all photos', loop: true, download: false, captions: true, gridCaptions: true, sort: 'name', class: 'my-gallery'}) }}Gallery Options:
| Option | Type | Default | Description |
|---|---|---|---|
collection | string | 'gallery' | Collection identifier |
property | string | 'gallery' | Property name |
maxVisible | int | 0 | Max visible thumbnails (0 = all) |
viewAllText | string | 'View All' | Text for the “view all” button |
captions | bool|string | false | Lightbox captions: true for default, or a template string |
gridCaptions | bool|string | false | Grid captions below thumbnails |
sort | string|array | '' | Sort property (prefix with - for descending) or array of sort rules |
class | string | '' | CSS class on the gallery container |
loop | bool | true | Loop gallery in lightbox |
download | bool | true | Show download button in lightbox |
counter | bool | true | Show image counter |
plugins | array | ['zoom','thumbnail','fullscreen'] | LightGallery plugins |
zoomFromOrigin | bool | true | Animate the lightbox open and close from the clicked thumbnail. Set false when thumbnails are cropped to a different shape than the originals — see below |
Every option that is not one of Total CMS’s own (collection, property, captions, gridCaptions, sort, class, maxVisible, viewAllText, featuredOnly) is passed straight through to LightGallery’s settings, so loop, download, counter, zoomFromOrigin, speed, mode and the rest all work as documented there.
Cropped thumbnails and the zoom animation. LightGallery’s opening animation grows the thumbnail into the full image. It assumes both have the same proportions: when the grid is square crops of landscape or portrait photos ({w: 300, h: 300, fit: 'crop'}), the thumbnail is stretched to the full image’s shape during the zoom and snaps back once the image loads (lightGallery #1698). Turn the thumbnail zoom off for those galleries and the full image fades in instead:
{{ cms.render.gallery('vacation', {w: 300, h: 300, fit: 'crop'}, {}, {zoomFromOrigin: false}) }}For caption templates and sorting details, see totalcms.md.
galleryLauncher()
Section titled “galleryLauncher()”Generate a hidden template element with gallery data for programmatic lightbox initialization. Use this when you want custom trigger elements instead of the default grid.
{{ cms.render.galleryLauncher('vacation') }}<button data-gallery="gallery-vacation">View Photos</button>
{# With settings #}{{ cms.render.galleryLauncher('vacation', {w: 300}, {w: 1920}, { captions: true, trigger: '.gallery-thumb', loop: true, download: false}) }}Additional Launcher Options:
| Option | Type | Default | Description |
|---|---|---|---|
trigger | string | '' | CSS selector for trigger elements |
galleryId | string | auto | Custom gallery ID (default: {collection}-{id}) |
sort | string | '' | Sort images by field (e.g., name, -name for descending) |
include | string | '' | Include filter — only images matching ALL criteria (e.g., tags:landscape,featured:true) |
exclude | string | '' | Exclude filter — remove images matching ANY criteria (e.g., tags:archived) |
search | string | '' | Full-text search across all image fields |
{# Filter to only landscape-tagged images #}{{ cms.render.galleryLauncher('vacation', {w: 300}, {w: 1920}, { include: 'tags:landscape'}) }}
{# Exclude archived images, sorted by name #}{{ cms.render.galleryLauncher('vacation', {w: 300}, {w: 1920}, { exclude: 'tags:archived', sort: 'name'}) }}
{# Search for images matching "sunset" #}{{ cms.render.galleryLauncher('vacation', {w: 300}, {w: 1920}, { search: 'sunset'}) }}Filtering uses the same syntax as Index Filtering, including wildcard patterns for flexible string matching.
For complete launcher usage with trigger methods and opening at specific images, see totalcms.md.
galleryImage()
Section titled “galleryImage()”Render a single gallery image as an <img> tag with LightGallery integration attributes (data-gallery, data-gallery-image).
{# By filename #}{{ cms.render.galleryImage('vacation', 'sunset.jpg', {w: 300, h: 200}) }}
{# Dynamic selectors #}{{ cms.render.galleryImage('vacation', 'first', {w: 400}) }}{{ cms.render.galleryImage('vacation', 'last') }}{{ cms.render.galleryImage('vacation', 'random') }}{{ cms.render.galleryImage('vacation', 'featured') }}galleryAlt()
Section titled “galleryAlt()”Get the alt text for a specific gallery image.
{{ cms.render.galleryAlt('vacation', 'sunset.jpg') }}{{ cms.render.galleryAlt('vacation', 'sunset.jpg', {collection: 'photos'}) }}galleryCaption()
Section titled “galleryCaption()”Get the caption for a gallery image, with optional template rendering.
{# Default caption (alt text fallback chain) #}{{ cms.render.galleryCaption('vacation', 'sunset.jpg') }}
{# With template #}{{ cms.render.galleryCaption('vacation', 'sunset.jpg', {}, '<h4>{alt}</h4><p>{exif.camera}</p>') }}Pagination
Section titled “Pagination”paginationSimple()
Section titled “paginationSimple()”Render simple Previous/Next pagination links.
{{ cms.render.paginationSimple(totalItems, currentPage, itemsPerPage) }}
{# With custom labels and page key #}{{ cms.render.paginationSimple(items|length, page, 10, 'page', 'Prev', 'Next') }}
{# Preserve query string parameters #}{{ cms.render.paginationSimple(total, page, 10, 'p', 'Previous', 'Next', {sort: 'date', q: query}) }}| Parameter | Type | Default | Description |
|---|---|---|---|
totalObjects | int | required | Total number of items |
currentPage | int | required | Current page number |
pageLimit | int | required | Items per page |
pageKey | string | 'p' | Query parameter name for page number |
prevContent | string | 'Previous' | Previous button text/HTML |
nextContent | string | 'Next' | Next button text/HTML |
getData | array | [] | Additional query parameters to preserve |
paginationFull()
Section titled “paginationFull()”Render full pagination with numbered page links, plus Previous/Next.
{{ cms.render.paginationFull(totalItems, currentPage, itemsPerPage) }}
{{ cms.render.paginationFull(items|length, page, 10, 'p', '← Previous', 'Next →', {sort: 'date'}) }}Parameters are identical to paginationSimple().
Load More
Section titled “Load More”loadMore()
Section titled “loadMore()”Generate an HTMX-powered “load more” trigger for paginated collection loading.
{{ cms.render.loadMore('blog', {template: 'blog/card.twig', limit: 10}) }}
{# Render first page server-side + HTMX for the rest #}{{ cms.render.loadMore('blog', {template: 'blog/card.twig', limit: 10, load: true}) }}
{# With empty state for filtered results #}{{ cms.render.loadMore('blog', { template: 'blog/card.twig', include: 'published:true', empty: '<p>No posts found.</p>'}) }}See Load More Documentation for full options and examples.
loadMoreDataView()
Section titled “loadMoreDataView()”Generate an HTMX-powered “load more” trigger for paginated DataView loading.
{{ cms.render.loadMoreDataView('recent-posts', {template: 'blog/card.twig', limit: 10}) }}loadMoreButton()
Section titled “loadMoreButton()”Generate a standalone HTMX button that appends items into an external container. Unlike loadMore(), the button can be placed anywhere on the page.
<div id="blog-feed"></div>{{ cms.render.loadMoreButton('blog', { target: '#blog-feed', template: 'blog/card.twig', limit: 10, load: true}) }}See Load More Documentation for full options and examples.
loadMoreDataViewButton()
Section titled “loadMoreDataViewButton()”Generate a standalone HTMX button for paginated DataView loading into an external container.
<div id="feed"></div>{{ cms.render.loadMoreDataViewButton('recent-posts', { target: '#feed', template: 'cards/item.twig', limit: 20}) }}Fragment URLs
Section titled “Fragment URLs”The HTML-fragment endpoints behind Load More, exposed for your own hx-get
and hx-post attributes. Each returns a raw URL; Twig’s autoescape handles it
inside an attribute. See HTMX Recipes for complete examples.
queryUrl()
Section titled “queryUrl()”{{ cms.render.queryUrl('blog', {template: 'blog/card', limit: 12, search: 'php'}) }}Options: template (required), limit, offset, sort, include, exclude, search, mode.
viewQueryUrl()
Section titled “viewQueryUrl()”{{ cms.render.viewQueryUrl('recent-posts', {template: 'cards/item', limit: 10}) }}objectFragmentUrl()
Section titled “objectFragmentUrl()”{{ cms.render.objectFragmentUrl('products', product.id, {template: 'products/quick-view'}) }}saveUrl()
Section titled “saveUrl()”{{ cms.render.saveUrl('contact', {template: 'forms/thanks'}) }} {# hx-post target #}{{ cms.render.saveUrl('posts', {id: post.id, template: 'posts/card'}) }} {# hx-put / hx-patch target #}incrementUrl()
Section titled “incrementUrl()”{{ cms.render.incrementUrl('posts', post.id, 'likes') }}Depot Browser
Section titled “Depot Browser”depotBrowser()
Section titled “depotBrowser()”Render an interactive file browser for depot properties. Displays files in a nested folder tree with filtering, previews, and download links.
{# Basic depot browser #}{{ cms.render.depotBrowser('my-object') }}
{# With options #}{{ cms.render.depotBrowser('my-object', { collection: 'documents', property: 'files', filter: true, preview: true, tags: true, comments: true, class: 'my-depot'}) }}
{# Filter by tags #}{{ cms.render.depotBrowser('my-object', {filterTags: ['important', 'public']}) }}
{# Flat file list (no folders) #}{{ cms.render.depotBrowser('my-object', {folders: false}) }}Depot Browser Options:
| Option | Type | Default | Description |
|---|---|---|---|
collection | string | 'depot' | Collection identifier |
property | string | 'depot' | Property name |
filter | bool | false | Show search/filter input |
preview | bool | false | Show preview buttons with modal |
comments | bool | false | Display file comments |
download | bool | true | Make files downloadable (vs stream) |
tags | bool | false | Display file tags |
folders | bool | true | Preserve folder structure |
humanize | bool | true | Convert filenames to title case |
class | string | '' | Custom CSS class |
reverseSort | bool | false | Reverse file sort order |
filterTags | array | [] | Filter files by tags (OR logic, case-insensitive) |
Admin Helpers
Section titled “Admin Helpers”cloneDialog()
Section titled “cloneDialog()”Render a clone dialog for duplicating objects in a collection. Used in the admin interface.
{{ cms.render.cloneDialog('blog') }}Grid Helpers
Section titled “Grid Helpers”The render adapter includes a grid sub-object with helper methods for content grids:
{{ cms.render.grid.date(item.date, 'M j, Y') }} {# Format date with fallback #}{{ cms.render.grid.tags(item.tags, '/blog/tag') }} {# Render tag list with links #}{{ cms.render.grid.excerpt(item.summary, 160) }} {# Generate text excerpt #}{{ cms.render.grid.price(item.price) }} {# Format price #}{{ cms.render.grid.meta(item.author) }} {# Render metadata (author, date) #}See cmsgrid tag for the {% cmsgrid %} template tag documentation.