Template Syntax
Mustache template syntax reference for custom plugins.
Overview
Custom plugin HTML templates use Mustache syntax for dynamic content. This provides a simple, logic-less templating system that keeps complex logic in JavaScript where it belongs.
⚠️ The {{variable}} template syntax ONLY works in HTML and CSS. It does NOT work in JavaScript!
<h1>{{title}}</h1>.card { background: {{backgroundColor}}; }const title = "{{title}}";(doesn't work!)const config = window.mp4e?.getConfig();const title = config.title;Variable Interpolation
Use double curly braces to insert config values into your template:
1<!-- Basic interpolation -->2<h1>{{title}}</h1>3<p>{{description}}</p>45<!-- Nested properties -->6<span class="price">{{product.price}}</span>7<span class="currency">{{product.currency}}</span>89<!-- Array access -->10<p>First item: {{items.0}}</p>1112<!-- With default values (via JS) -->13<span>{{title}}</span> <!-- Falls back to empty string if undefined -->Playback State Variables
The engine injects playback state as system variables, available in any {{...}} expression. These update automatically every frame during playback.
| Variable | Type | Description |
|---|---|---|
| {{currentFrame}} | number | Current frame number (1-based) |
| {{currentTime}} | number | Current playback time in seconds |
| {{duration}} | number | Total video duration in seconds |
| {{totalFrames}} | number | Total number of frames |
| {{fps}} | number | Video frame rate |
| {{isPlaying}} | boolean | Whether video is currently playing |
| {{playbackRate}} | number | Current playback speed (1 = normal) |
| {{volume}} | number | Current volume level (0-1) |
| {{isMuted}} | boolean | Whether audio is muted |
| {{isFullscreen}} | boolean | Whether player is in fullscreen mode |
1<!-- Playback state variables are injected by the engine and update every frame -->23<!-- Current position -->4<span>Frame: {{currentFrame}}</span>5<span>Time: {{currentTime}}s</span>67<!-- Video metadata -->8<span>Duration: {{duration}}s</span>9<span>Total Frames: {{totalFrames}}</span>10<span>FPS: {{fps}}</span>1112<!-- Playback state -->13<span>Playing: {{isPlaying}}</span>14<span>Rate: {{playbackRate}}x</span>15<span>Volume: {{volume}}</span>16<span>Muted: {{isMuted}}</span>17<span>Fullscreen: {{isFullscreen}}</span>1819<!-- CSS custom properties driven by playback state -->20<style>21 .progress { width: calc({{currentTime}} / {{duration}} * 100%); }22</style>Math Expressions
Template expressions support inline arithmetic. You can use +, -, *, /, % (modulo) and parentheses to build calculated values:
1<!-- Inline arithmetic inside {{...}} templates -->2<!-- Supported operators: + - * / % (modulo) and parentheses -->34<!-- Cycle hue through 360 degrees based on frame -->5<div style="filter: hue-rotate({{(currentFrame * 4) % 360}}deg)">6 Color-cycling content7</div>89<!-- Calculate percentage -->10<span>{{score * 100 / maxScore}}%</span>1112<!-- Combine variables with math -->13<span>Total: {{price * quantity + shippingCost}}</span>1415<!-- Nested parentheses -->16<span>{{(baseScore + bonus) * multiplier}}</span>1718<!-- Mix playback state with project variables -->19<span>Frame offset: {{currentFrame - startFrame}}</span>Object Data Variables
For object display plugins (tooltips, product cards attached to detected objects), you can access the object's properties and custom data fields:
1<!-- Object Display Plugins: Access object data -->23<!-- Built-in object properties -->4<h1>{{object.label}}</h1> <!-- AI-detected label: "chair", "person" -->5<h2>{{object.userLabel}}</h2> <!-- User-defined label -->6<span>ID: {{object.id}}</span> <!-- Unique object ID -->78<!-- Custom data fields (defined in Group Data Schema) -->9<h3>{{object.data.title}}</h3>10<p>{{object.data.description}}</p>11<span class="price">${{object.data.price}}</span>12<span class="currency">{{object.data.currency}}</span>13<a href="{{object.data.url}}">Buy Now</a>14<img src="{{object.data.imageUrl}}" alt="{{object.data.title}}">1516<!-- Fallback operator: use first non-empty value -->17<h1>{{object.data.title || object.userLabel || object.label}}</h1>1819<!-- Example: Product tooltip for detected object -->20<div class="product-tooltip">21 <img src="{{object.data.imageUrl}}" alt="{{object.data.title}}">22 <div class="info">23 <h3>{{object.data.title || object.userLabel}}</h3>24 <p>{{object.data.description}}</p>25 <span class="price">${{object.data.price}} {{object.data.currency}}</span>26 </div>27</div>object.data.title are defined in the Group Settings → Data Schema tab. Each group can define its own fields (e.g., e-commerce groups might have price, URL; educational groups might have difficulty, duration).Conditionals
Sections render content based on truthiness. Use # for truthy and ^ for falsy:
1<!-- Section: Renders if truthy -->2{{#showImage}}3<img src="{{imageUrl}}" alt="{{title}}">4{{/showImage}}56<!-- Inverted section: Renders if falsy -->7{{^showImage}}8<div class="placeholder">9 <span>No image available</span>10</div>11{{/showImage}}1213<!-- Boolean checks -->14{{#isOnSale}}15<span class="badge sale">SALE</span>16{{/isOnSale}}1718{{^isOnSale}}19<span class="badge">Regular Price</span>20{{/isOnSale}}2122<!-- Existence check (renders if defined and non-empty) -->23{{#discount}}24<span class="discount">-{{discount}}%</span>25{{/discount}}2627<!-- Nested conditionals -->28{{#product}}29 {{#product.inStock}}30 <button>Add to Cart</button>31 {{/product.inStock}}32 {{^product.inStock}}33 <button disabled>Out of Stock</button>34 {{/product.inStock}}35{{/product}}Loops & Iteration
Iterate over arrays using sections. Inside the loop, properties are accessible directly:
1<!-- Iterating over arrays -->2<ul class="features">3 {{#features}}4 <li>{{.}}</li>5 {{/features}}6</ul>78<!-- Array of objects -->9<div class="products">10 {{#products}}11 <div class="product-card">12 <h3>{{name}}</h3>13 <p>{{description}}</p>14 <span class="price">${{price}}</span>15 </div>16 {{/products}}17</div>1819<!-- Empty array fallback -->20{{#products}}21<div class="product">{{name}}</div>22{{/products}}23{{^products}}24<p class="empty">No products available</p>25{{/products}}2627<!-- Nested loops -->28{{#categories}}29<div class="category">30 <h2>{{name}}</h2>31 <ul>32 {{#items}}33 <li>{{title}} - {{price}}</li>34 {{/items}}35 </ul>36</div>37{{/categories}}3839<!-- Current item reference with . -->40{{#tags}}41<span class="tag">{{.}}</span>42{{/tags}}HTML Escaping
By default, values are HTML-escaped to prevent XSS. Use triple braces for trusted HTML:
1<!-- Default: HTML-escaped (safe) -->2<p>{{userInput}}</p>3<!-- Input: "<script>alert('xss')</script>" -->4<!-- Output: <script>alert('xss')</script> -->56<!-- Triple mustache: Unescaped HTML (use carefully!) -->7<div class="rich-content">{{{richHtml}}}</div>8<!-- Input: "<strong>Bold text</strong>" -->9<!-- Output: <strong>Bold text</strong> -->1011<!-- When to use unescaped -->12<!-- SAFE: Content from your CMS/database that you control -->13{{{trustedHtmlContent}}}1415<!-- DANGEROUS: Never use with user input! -->16<!-- {{{userProvidedHtml}}} <-- DON'T DO THIS -->1718<!-- Alternative: Use JS for dynamic content -->19<div id="content"></div>20<!-- In JS: document.getElementById('content').textContent = userInput; -->{{{}}} with user-provided content. Only use it for trusted HTML from your own systems.Context Variables
Access player context through the special _context variable:
1<!-- Playback state variables (engine-injected, available everywhere) -->2<span>Time: {{currentTime}}s</span>3<span>Frame: {{currentFrame}}</span>4<span>Duration: {{duration}}s</span>56<!-- Conditionals with playback state -->7{{#isPlaying}}8<span class="status">Playing</span>9{{/isPlaying}}10{{^isPlaying}}11<span class="status">Paused</span>12{{/isPlaying}}1314<!-- Project variables (user-defined) -->15<span>Cart: {{cartCount}} items</span>16<span>Score: {{quizScore}}</span>1718<!-- Object context (when overlay is bound to an object) -->19<div class="object-info">20 <span>{{object.label}}</span>21 <span>{{object.userLabel}}</span>22</div>2324<!-- Math expressions with variables -->25<div style="opacity: {{currentTime / duration}}">26 Fading content27</div>Helper Functions
Use JavaScript helper functions for formatting and transformations:
1// Helper functions available in JavaScript23// Format numbers4mp4e.helpers.formatNumber(1234.56); // "1,234.56"5mp4e.helpers.formatCurrency(99.99, 'USD'); // "$99.99"6mp4e.helpers.formatPercent(0.156); // "15.6%"78// Format dates9mp4e.helpers.formatDate(date, 'short'); // "12/13/2025"10mp4e.helpers.formatDate(date, 'long'); // "December 13, 2025"11mp4e.helpers.formatTime(seconds); // "1:23" or "1:23:45"12mp4e.helpers.formatRelativeTime(date); // "2 hours ago"1314// String helpers15mp4e.helpers.truncate(text, 50); // Truncate to 50 chars16mp4e.helpers.capitalize(text); // Capitalize first letter17mp4e.helpers.slugify(text); // "Hello World" -> "hello-world"1819// Array helpers20mp4e.helpers.first(array); // First element21mp4e.helpers.last(array); // Last element22mp4e.helpers.take(array, 3); // First 3 elements23mp4e.helpers.shuffle(array); // Randomize order2425// Using helpers to update DOM26var cfg = mp4e.getConfig();27var priceEl = document.getElementById('price');28priceEl.textContent = mp4e.helpers.formatCurrency(cfg.price, cfg.currency);2930// Listen for variable changes to update UI31mp4e.onVariableChange(function(variables, changedIds) {32 // Update only when relevant variable changes33 if (changedIds && changedIds.indexOf('currentTime') < 0) return;34 var timeEl = document.getElementById('video-time');35 timeEl.textContent = mp4e.helpers.formatTime(variables.currentTime);36});Complete Examples
A comprehensive product card plugin using all template features:
1<!-- Product Card Template -->2{3 template: {4 html: `5 <div class="product-card">6 {{#imageUrl}}7 <img src="{{imageUrl}}" alt="{{title}}" class="product-image">8 {{/imageUrl}}9 {{^imageUrl}}10 <div class="product-image placeholder">11 <svg viewBox="0 0 24 24" fill="currentColor">12 <path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"/>13 </svg>14 </div>15 {{/imageUrl}}1617 <div class="product-content">18 <h3 class="product-title">{{title}}</h3>1920 {{#subtitle}}21 <p class="product-subtitle">{{subtitle}}</p>22 {{/subtitle}}2324 <div class="product-pricing">25 {{#originalPrice}}26 <span class="original-price">${{originalPrice}}</span>27 {{/originalPrice}}28 <span class="current-price">${{price}}</span>29 {{#discount}}30 <span class="discount-badge">-{{discount}}%</span>31 {{/discount}}32 </div>3334 {{#rating}}35 <div class="product-rating">36 <div class="stars" style="--rating: {{rating}}"></div>37 <span class="rating-count">({{reviewCount}})</span>38 </div>39 {{/rating}}4041 {{#tags}}42 <div class="product-tags">43 {{#tags}}44 <span class="tag">{{.}}</span>45 {{/tags}}46 </div>47 {{/tags}}4849 <button id="add-to-cart" class="btn-primary">50 {{#inStock}}51 Add to Cart52 {{/inStock}}53 {{^inStock}}54 Out of Stock55 {{/inStock}}56 </button>57 </div>58 </div>59 `,60 css: `61 .product-card {62 background: white;63 border-radius: 12px;64 overflow: hidden;65 box-shadow: 0 4px 12px rgba(0,0,0,0.1);66 max-width: 300px;67 }68 .product-image {69 width: 100%;70 height: 200px;71 object-fit: cover;72 }73 .product-image.placeholder {74 background: #f3f4f6;75 display: flex;76 align-items: center;77 justify-content: center;78 color: #9ca3af;79 }80 .product-image.placeholder svg {81 width: 48px;82 height: 48px;83 }84 .product-content {85 padding: 16px;86 }87 .product-title {88 margin: 0 0 4px;89 font-size: 18px;90 font-weight: 600;91 color: #111827;92 }93 .product-subtitle {94 margin: 0 0 12px;95 font-size: 14px;96 color: #6b7280;97 }98 .product-pricing {99 display: flex;100 align-items: center;101 gap: 8px;102 margin-bottom: 12px;103 }104 .original-price {105 text-decoration: line-through;106 color: #9ca3af;107 font-size: 14px;108 }109 .current-price {110 font-size: 24px;111 font-weight: 700;112 color: #111827;113 }114 .discount-badge {115 background: #fef2f2;116 color: #ef4444;117 padding: 2px 8px;118 border-radius: 4px;119 font-size: 12px;120 font-weight: 600;121 }122 .product-rating {123 display: flex;124 align-items: center;125 gap: 8px;126 margin-bottom: 12px;127 }128 .stars {129 --percent: calc(var(--rating) / 5 * 100%);130 display: inline-block;131 font-size: 14px;132 line-height: 1;133 background: linear-gradient(90deg, #fbbf24 var(--percent), #e5e7eb var(--percent));134 -webkit-background-clip: text;135 background-clip: text;136 }137 .stars::before {138 content: '★★★★★';139 -webkit-text-fill-color: transparent;140 }141 .rating-count {142 font-size: 12px;143 color: #6b7280;144 }145 .product-tags {146 display: flex;147 flex-wrap: wrap;148 gap: 6px;149 margin-bottom: 12px;150 }151 .tag {152 background: #f3f4f6;153 color: #4b5563;154 padding: 4px 8px;155 border-radius: 4px;156 font-size: 12px;157 }158 .btn-primary {159 width: 100%;160 padding: 12px 16px;161 background: #3b82f6;162 color: white;163 border: none;164 border-radius: 8px;165 font-size: 16px;166 font-weight: 600;167 cursor: pointer;168 transition: background 0.2s;169 }170 .btn-primary:hover {171 background: #2563eb;172 }173 .btn-primary:disabled {174 background: #9ca3af;175 cursor: not-allowed;176 }177 `,178 js: `179 var button = document.getElementById('add-to-cart');180 var cfg = mp4e.getConfig();181182 if (!cfg.inStock) {183 button.disabled = true;184 }185186 button.addEventListener('click', function() {187 mp4e.emit('onAddToCart', {188 productId: cfg.productId,189 title: cfg.title,190 price: cfg.price191 });192193 mp4e.executeActions([194 { type: 'setVariable', variableId: 'cartCount', operation: 'increment', value: 1 },195 { type: 'showNotification', message: 'Added to cart!' }196 ]);197198 // Visual feedback199 button.textContent = 'Added!';200 button.style.background = '#22c55e';201 setTimeout(() => {202 button.textContent = 'Add to Cart';203 button.style.background = '';204 }, 2000);205 });206 `207 }208}