HTL Cheat Sheet & Complete Reference (Sightly)

12 min read

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.

AEMHTLSightlyFrontendCheat SheetReference
HTL Cheat Sheet & Complete Reference (Sightly)

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:

VariableMeaning
itemList.index0-based position
itemList.count1-based position
itemList.firsttrue on the first item
itemList.lasttrue on the last item
itemList.odd / itemList.evenAlternation (by index)
itemList.middleNot 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.

ContextUse forExample
htmlRich markup (filters dangerous tags)${markup @ context='html'}
textPlain text (default in element body)${value @ context='text'}
attributeAttribute values (default in attrs)${v @ context='attribute'}
urihref/src URLs${link @ context='uri'}
numberNumeric output${n @ context='number'}
scriptStringInside a JS string${v @ context='scriptString'}
scriptTokenA JS identifier/keyword${v @ context='scriptToken'}
styleStringInside a CSS string${v @ context='styleString'}
styleTokenA CSS identifier${v @ context='styleToken'}
commentHTML comment${v @ context='comment'}
unsafeDisables 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.

ObjectWhat it is
propertiesThe current resource's properties (ValueMap)
pagePropertiesThe current page's jcr:content properties
inheritedPagePropertiesPage properties inherited up the tree
resourceThe current Resource
currentPageThe current Page
currentDesign / currentStyleDesign/policy info
wcmmodeAuthoring mode (wcmmode.edit, .disabled, .preview)
request / responseThe Sling request/response
slingSlingScriptHelper
componentThe current component definition
componentContextRendering context
editContextEdit context (authoring)
currentSession / userPreferencesJCR 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 context when outputting into URLs, JavaScript, or CSS.
  • ✅ Use <sly> for logic-only blocks so no wrapper tag leaks into the markup.
  • ✅ Assign data-sly-test results to variables to avoid recomputing the same condition.
  • ✅ Wrap every author- or user-facing string in i18n.
  • ✅ Use data-sly-list to repeat children and data-sly-repeat to repeat the element itself.

Do's and Don'ts

Do

  • ✅ Bind models with data-sly-use and keep templates declarative.
  • ✅ Reuse markup with data-sly-template and data-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.

Share this article

Subscribe to the Newsletter

Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.

Back to Blog