HTL Cheat Sheet & Complete Reference (Sightly)
The complete HTL (Sightly) reference for AEM — every block statement (data-sly-use, test, list, repeat, resource, include, template, call, attribute, element, unwrap, set), expression options, display contexts for XSS, global objects, and a copy-paste cheat sheet.

HTL — the HTML Template Language, originally called Sightly — is the language you use to produce markup in AEM. Adobe designed it to be deliberately small and deliberately safe: it has only a handful of data-sly-* block statements and a compact expression language, and it escapes output automatically so that cross-site scripting is hard to introduce by accident. That small surface area is a feature, but it also means the few details that exist — display contexts, expression options, the loop helper variables — carry a lot of weight, and they're exactly where developers get tripped up.
This article is the complete reference and a practical cheat sheet. Every block statement is explained with an example, the escaping model is covered in full, and the global objects you can reach from any template are listed. Read it once end to end to build a mental model, then come back to the cheat sheet at the bottom whenever you need a quick reminder.
It's the natural companion to the Component Development guide, which shows HTL alongside the dialog and model it renders; for the Java behind it, see the Annotations reference.
The expression language
Everything dynamic in HTL happens inside ${ }. An expression can read a property, call a getter, perform basic logic, and accept options after an @ symbol. The syntax is intentionally close to plain JavaScript, so most of it reads exactly the way you'd expect:
${properties.title} <!-- property access -->
${teaser.heading} <!-- model getter (getHeading) -->
${properties['jcr:title']} <!-- bracket access -->
${currentPage.title || 'Untitled'} <!-- logical OR / default -->
${a && b} ${a || b} ${!a} <!-- boolean logic -->
${x > 5} ${x == y} ${x != y} <!-- comparison -->
${cond ? 'yes' : 'no'} <!-- ternary -->
${'Hello ' + name} <!-- string concat -->
${[1, 2, 3]} <!-- array literal -->
The one piece of "magic" to internalize is that property access automatically calls getters. Writing ${model.title} actually invokes getTitle() on your Sling Model (or isTitle() if the property is a boolean). You never call methods directly — you express the data you want, and HTL fetches it.
Block statements (data-sly-*)
Block statements are HTML attributes prefixed with data-sly- that give an element behavior — binding logic, looping, conditionally rendering, and so on. There are only about a dozen, and you'll use the same five or six constantly.
data-sly-use — bind logic
data-sly-use connects your template to its logic by loading a Sling Model (or a Java/JavaScript Use-API object) into a named variable you can then reference.
<sly data-sly-use.teaser="com.mysite.core.models.TeaserModel"/>
<div data-sly-use.nav="com.mysite.core.models.Navigation">${nav.title}</div>
<!-- pass parameters to the Use object -->
<sly data-sly-use.list="${'com.mysite.core.models.List' @ limit=10, tag='news'}"/>
The part after the dot (teaser, nav, list) is the variable name the rest of the template uses. The last example shows that you can pass parameters into the object with the @ option syntax — handy for generic, reusable models.
data-sly-text — set text content
data-sly-text replaces an element's content with an expression's value, escaped as plain text. It's the safe, explicit way to output text.
<p data-sly-text="${teaser.description}">placeholder</p>
The placeholder content is useful during development — it shows in a static HTML preview and is replaced at render time.
data-sly-attribute — set attributes
Use data-sly-attribute to set an attribute's value dynamically — or, with no attribute name, to apply a whole map of attributes at once.
<a data-sly-attribute.href="${teaser.link}"
data-sly-attribute.title="${teaser.heading}">Link</a>
<!-- set multiple attributes from a map -->
<div data-sly-attribute="${teaser.attributes}"></div>
A nice side effect of this approach is that an attribute whose value resolves to empty or null is simply omitted, so you never render an empty href="".
data-sly-element — change the tag name
data-sly-element swaps the element's tag at render time — useful when an author chooses a heading level, for example.
<h2 data-sly-element="${headingLevel}">${title}</h2> <!-- e.g. renders <h3> -->
data-sly-test — conditional rendering
data-sly-test renders its element only when the condition is truthy. You can also assign the test's result to a variable and reuse it elsewhere, which avoids computing the same condition twice.
<p data-sly-test="${teaser.heading}">${teaser.heading}</p>
<!-- assign + reuse -->
<div data-sly-test.hasLinks="${teaser.links.size > 0}">…</div>
<p data-sly-test="${!hasLinks}">No links</p>
data-sly-list & data-sly-repeat — iteration
Both statements iterate a collection; the difference is what repeats. data-sly-list repeats the element's children, while data-sly-repeat repeats the element itself, tag and all.
<ul data-sly-list.item="${teaser.links}">
<li>${itemList.index}: <a href="${item.url}">${item.text}</a></li>
</ul>
<li data-sly-repeat.item="${teaser.links}">${item.text}</li>
Inside any loop, HTL exposes a helper object that tells you where you are in the iteration — invaluable for "first/last" styling or zebra striping:
| Variable | Meaning |
|---|---|
itemList.index | 0-based position |
itemList.count | 1-based position |
itemList.first | true on the first item |
itemList.last | true on the last item |
itemList.odd / itemList.even | Alternation (by index) |
itemList.middle | Not first and not last |
The helper is named after your loop variable: if you write data-sly-list.row, the helper is rowList.
data-sly-resource — include another resource
data-sly-resource renders another resource (by path), letting AEM resolve and render it as its own component. This is how containers render their children and how you compose pages from parts.
<div data-sly-resource="${'child' @ resourceType='mysite/components/text'}"></div>
<div data-sly-resource="${resourcePath @
resourceType='mysite/components/teaser',
selectors='mobile',
appendPath='/jcr:content',
wcmmode='disabled',
decorationTagName='section'}"></div>
The options give you fine control over how the included resource is rendered. The most common are resourceType, selectors (plus addSelectors / removeSelectors), appendPath / prependPath, wcmmode, and decorationTagName / cssClassName for the wrapper element AEM generates.
data-sly-include — include a script/file
Where data-sly-resource includes a resource, data-sly-include includes the output of another script directly — a header file, a shared snippet — without going through resource resolution.
<sly data-sly-include="header.html"/>
<sly data-sly-include="${'content.html' @ wcmmode='disabled'}"/>
data-sly-template & data-sly-call — reusable fragments
Templates are HTL's version of functions: data-sly-template defines a named, parameterized block of markup, and data-sly-call renders it with arguments. They're the right way to avoid copy-pasting markup.
<!-- define -->
<template data-sly-template.card="${@ title, href}">
<article class="card">
<h3>${title}</h3>
<a href="${href}">Read</a>
</article>
</template>
<!-- call -->
<sly data-sly-call="${card @ title='News', href='/news'}"/>
<!-- templates from another file -->
<sly data-sly-use.lib="library.html"
data-sly-call="${lib.card @ title='X', href='/x'}"/>
Defining templates in a separate file and pulling them in with data-sly-use gives you a shared "component library" of markup snippets you can reuse across templates.
data-sly-unwrap — drop the wrapper tag
data-sly-unwrap removes the host element from the output but keeps its content — useful when you need a statement to attach to something but don't want an extra tag in the markup. The <sly> element is unwrapped automatically, which is why you see it used for logic-only blocks.
<div data-sly-unwrap>kept, but no div</div>
<sly data-sly-test="${cond}">no wrapper tag at all</sly>
data-sly-set — assign a variable
data-sly-set stores the result of an expression in a variable so you can compute something once and reuse it.
<sly data-sly-set.fullName="${first + ' ' + last}"/>
<p>${fullName}</p>
Display contexts (XSS protection)
This is the most important section of the entire reference, because it's what makes HTL secure by default. HTL automatically escapes every expression, and it chooses how to escape based on where the expression appears in the markup — text inside an element is escaped differently from a URL in an href, which is escaped differently from a value inside a <script> block. You can override the choice with the @ context='...' option when HTL can't infer the right one.
Treat this table as a security checklist. The only entry that disables protection is unsafe, and you should reach for it only when the value is something you produced and fully trust.
| Context | Use for | Example |
|---|---|---|
html | Rich markup (filters dangerous tags) | ${markup @ context='html'} |
text | Plain text (default in element body) | ${value @ context='text'} |
attribute | Attribute values (default in attrs) | ${v @ context='attribute'} |
uri | href/src URLs | ${link @ context='uri'} |
number | Numeric output | ${n @ context='number'} |
scriptString | Inside a JS string | ${v @ context='scriptString'} |
scriptToken | A JS identifier/keyword | ${v @ context='scriptToken'} |
styleString | Inside a CSS string | ${v @ context='styleString'} |
styleToken | A CSS identifier | ${v @ context='styleToken'} |
comment | HTML comment | ${v @ context='comment'} |
unsafe | Disables escaping (dangerous) | ${trusted @ context='unsafe'} |
Expression options
Options are modifiers you append after @ to change how an expression resolves — translating it, formatting it, joining an array, and so on. You can combine several, separated by commas.
<!-- i18n translation -->
${'Welcome' @ i18n}
${'Welcome' @ i18n, hint='greeting', locale='fr'}
<!-- string formatting (placeholders {0}, {1}) -->
${'Page {0} of {1}' @ format=[current, total]}
<!-- date/number formatting -->
${date @ format='yyyy-MM-dd', timezone='UTC'}
<!-- join an array -->
${['a','b','c'] @ join=', '}
<!-- default when empty/null -->
${properties.title @ context='text'}
${value || 'fallback'}
<!-- boolean attribute (rendered only when true) -->
<input type="checkbox" data-sly-attribute.checked="${isChecked}"/>
The ones you'll use most are i18n (with its hint, locale, and source sub-options) for translation, format for string and date formatting, join for arrays, and of course context for escaping.
Global objects (bindings)
Every HTL script has access to a set of global objects without any data-sly-use declaration. These give you the current page, resource, request, and authoring state directly.
| Object | What it is |
|---|---|
properties | The current resource's properties (ValueMap) |
pageProperties | The current page's jcr:content properties |
inheritedPageProperties | Page properties inherited up the tree |
resource | The current Resource |
currentPage | The current Page |
currentDesign / currentStyle | Design/policy info |
wcmmode | Authoring mode (wcmmode.edit, .disabled, .preview) |
request / response | The Sling request/response |
sling | SlingScriptHelper |
component | The current component definition |
componentContext | Rendering context |
editContext | Edit context (authoring) |
currentSession / userPreferences | JCR session / prefs |
The two you'll reach for most are wcmmode (to render author-only UI) and currentPage/pageProperties (to read page-level data):
<div data-sly-test="${wcmmode.edit}">Editing mode UI</div>
<h1>${currentPage.title}</h1>
<meta name="template" content="${pageProperties['cq:template']}"/>
Comments
HTL has its own comment syntax, and the distinction from HTML comments matters: an HTL comment is removed entirely on the server, while an ordinary HTML comment is sent to the browser. Use HTL comments for developer notes so you don't leak anything to the page source.
<!--/* This HTL comment is removed server-side and supports ${expressions} */-->
<!-- This HTML comment is sent to the browser -->
Cheat sheet
Block statements
data-sly-use.x="model | script | ${'Class' @ param=val}"
data-sly-text="${value}"
data-sly-attribute.href="${url}" data-sly-attribute="${map}"
data-sly-element="${tagName}"
data-sly-test.cond="${expr}"
data-sly-list.item="${items}" <!-- itemList.index/count/first/last/odd/even/middle -->
data-sly-repeat.item="${items}" <!-- repeats the element itself -->
data-sly-resource="${path @ resourceType='...', selectors='...', wcmmode='disabled'}"
data-sly-include="${'file.html' @ wcmmode='disabled'}"
data-sly-template.tpl="${@ a, b}" / data-sly-call="${tpl @ a='x', b='y'}"
data-sly-unwrap / data-sly-set.var="${expr}"
Contexts & options
${v @ context='html|text|attribute|uri|scriptString|styleString|unsafe'}
${'Key' @ i18n, hint='...', locale='fr'}
${'{0} of {1}' @ format=[a, b]}
${list @ join=', '}
${date @ format='yyyy-MM-dd', timezone='UTC'}
Best practices
- ✅ Keep no business logic in HTL — compute in a Sling Model and expose getters.
- ✅ Trust the auto-escaping, and set an explicit
contextwhen outputting into URLs, JavaScript, or CSS. - ✅ Use
<sly>for logic-only blocks so no wrapper tag leaks into the markup. - ✅ Assign
data-sly-testresults to variables to avoid recomputing the same condition. - ✅ Wrap every author- or user-facing string in
i18n. - ✅ Use
data-sly-listto repeat children anddata-sly-repeatto repeat the element itself.
Do's and Don'ts
Do
- ✅ Bind models with
data-sly-useand keep templates declarative. - ✅ Reuse markup with
data-sly-templateanddata-sly-call. - ✅ Pass parameters into Use objects and templates with the
@syntax.
Don't
- ❌ Don't use
context='unsafe'on author or user input — it's a direct XSS hole. - ❌ Don't query the repository or call services from HTL.
- ❌ Don't leave debug HTML comments in the markup (they ship to the browser) — use HTL comments.
- ❌ Don't nest deep logic in templates; refactor it into the model or a sub-template.
Wrapping up
HTL stays small on purpose, and that constraint is what keeps AEM front ends clean and secure. The whole language comes down to a rhythm: bind a model with data-sly-use, output values with data-sly-text, loop and include with data-sly-list and data-sly-resource, share markup with templates, and let the display contexts handle escaping for you. Keep the logic in Java and the markup in HTL, and your components will be both maintainable and safe by default.
From here, see how HTL fits into a full build in the Component Development guide, go deeper on the Java in the Annotations reference, or step back to the big picture with the AEM Developer Cheat Sheet. To scaffold HTL and dialogs in seconds, try my AEM Component Generator.
Subscribe to the Newsletter
Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.