HTL Tutorial #11: data-sly-element - Dynamic HTML Tags
HTL Tutorial #11: data-sly-element - Dynamic HTML Tags
What is data-sly-element?
data-sly-element replaces the HTML tag of an element with another dynamically specified tag.
Syntax
<div data-sly-element="${tagName}">
Content
</div>Basic Example
<!-- The div becomes h2 -->
<div data-sly-element="h2">
Dynamic Title
</div>Output:
<h2>
Dynamic Title
</h2>Why Use It?
1. Dynamic Headings
Heading level based on page depth:
<div data-sly-use.page="com.example.PageModel"
data-sly-element="${page.headingLevel}">
${page.title}
</div>Java Model:
@Model(adaptables = Resource.class)
public class PageModel {
@Inject
private Page currentPage;
public String getHeadingLevel() {
int depth = currentPage.getDepth();
// Homepage: h1, Level 1: h2, Level 2: h3, etc.
int level = Math.min(depth, 6);
return "h" + level;
}
public String getTitle() {
return currentPage.getTitle();
}
}Output (depth=2):
<h2>
Example Page
</h2>2. Ordered vs Unordered Lists
<div data-sly-element="${properties.ordered ? 'ol' : 'ul'}">
<li data-sly-list.item="${properties.items}">
${item}
</li>
</div>If properties.ordered = true:
<ol>
<li>First</li>
<li>Second</li>
<li>Third</li>
</ol>If properties.ordered = false:
<ul>
<li>First</li>
<li>Second</li>
<li>Third</li>
</ul>3. Semantic HTML Elements
<div data-sly-element="${properties.semantic || 'div'}">
${properties.content}
</div>The author can choose: article, section, aside, nav, main, etc.
Safe Tag Whitelist
HTL has a whitelist of allowed HTML tags for security reasons. If you specify a tag not in the list, HTL uses <div> as a fallback.
Allowed Tags
a, abbr, address, article, aside, b, bdi, bdo, blockquote, br, caption,
cite, code, col, colgroup, data, dd, del, dfn, div, dl, dt, em, figcaption,
figure, footer, h1, h2, h3, h4, h5, h6, header, hr, i, ins, kbd, li, main,
mark, nav, ol, p, pre, q, rp, rt, ruby, s, samp, section, small, span,
strong, sub, sup, table, tbody, td, tfoot, th, thead, time, tr, u, ul, var,
wbrNOT Allowed Tags (Example)
<!-- BLOCKED - script is not in the whitelist -->
<div data-sly-element="script">
alert('XSS')
</div>
<!-- Output (fallback to div): -->
<div>
alert('XSS')
</div>This prevents XSS injection!
Common Patterns
1. Hierarchical Heading
<sly data-sly-template.heading="${@ level, text}">
<div data-sly-element="${'h' + level}">
${text}
</div>
</sly>
<!-- Usage -->
<sly data-sly-call="${heading @ level=1, text='Main Title'}"></sly>
<sly data-sly-call="${heading @ level=2, text='Subtitle'}"></sly>
<sly data-sly-call="${heading @ level=3, text='Section'}"></sly>Output:
<h1>Main Title</h1>
<h2>Subtitle</h2>
<h3>Section</h3>2. Link vs Span
<div data-sly-element="${link.url ? 'a' : 'span'}"
data-sly-attribute.href="${link.url}">
${link.text}
</div>With URL:
<a href="/page">Click here</a>Without URL:
<span>Plain text</span>3. Semantic Container
<div data-sly-use.comp="com.example.ContainerModel"
data-sly-element="${comp.semanticTag}"
data-sly-attribute.class="${comp.cssClass}">
<div data-sly-list.child="${comp.children}">
${child}
</div>
</div>Model:
public class ContainerModel {
@ValueMapValue
private String containerType; // "article", "section", "aside", "div"
public String getSemanticTag() {
if (containerType != null) {
return containerType;
}
return "div"; // default
}
public String getCssClass() {
return "container-" + getSemanticTag();
}
}Combining with Other Statements
With data-sly-test
<!-- Heading only if there's a title -->
<div data-sly-test="${properties.title}"
data-sly-element="${properties.headingLevel || 'h2'}"
data-sly-text="${properties.title}">
Placeholder
</div>With data-sly-list
<div data-sly-element="${properties.listType || 'ul'}">
<li data-sly-list.item="${properties.items}">
${item}
</li>
</div>With data-sly-attribute
<div data-sly-element="${properties.tag || 'div'}"
data-sly-attribute.id="${properties.id}"
data-sly-attribute.class="${properties.cssClass}">
${properties.content}
</div>Complete Example: Title Component
<div data-sly-use.title="com.example.TitleComponent"
data-sly-test="${title.text}"
data-sly-element="${title.type}"
data-sly-attribute.class="${'title ' + title.styleClass}"
data-sly-attribute.id="${title.anchorId}">
${title.text}
</div>Java Model:
@Model(adaptables = Resource.class)
public class TitleComponent {
@ValueMapValue
private String text;
@ValueMapValue
private String type; // h1, h2, h3, h4, h5, h6
@ValueMapValue
private String size; // small, medium, large
@ValueMapValue
private String anchorId;
public String getText() {
return text;
}
public String getType() {
return type != null ? type : "h2";
}
public String getStyleClass() {
String sizeClass = "";
if ("small".equals(size)) {
sizeClass = "title-sm";
} else if ("large".equals(size)) {
sizeClass = "title-lg";
}
return sizeClass;
}
public String getAnchorId() {
return anchorId;
}
}Dialog XML (_cq_dialog.xml snippet):
<type
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/select"
fieldLabel="Heading Type"
name="./type">
<items jcr:primaryType="nt:unstructured">
<h1 jcr:primaryType="nt:unstructured" text="H1" value="h1"/>
<h2 jcr:primaryType="nt:unstructured" text="H2" value="h2"/>
<h3 jcr:primaryType="nt:unstructured" text="H3" value="h3"/>
<h4 jcr:primaryType="nt:unstructured" text="H4" value="h4"/>
<h5 jcr:primaryType="nt:unstructured" text="H5" value="h5"/>
<h6 jcr:primaryType="nt:unstructured" text="H6" value="h6"/>
</items>
</type>Output (type=h3, size=large, anchorId=intro):
<h3 class="title title-lg" id="intro">
Introduction to the Project
</h3>Complete Example: Button Component
<div data-sly-use.button="com.example.ButtonComponent"
data-sly-element="${button.tag}"
data-sly-attribute.href="${button.href}"
data-sly-attribute.type="${button.type}"
data-sly-attribute.class="${button.classes}"
data-sly-attribute.target="${button.target}"
data-sly-attribute.rel="${button.rel}">
${button.text}
</div>Model:
public class ButtonComponent {
@ValueMapValue
private String text;
@ValueMapValue
private String link;
@ValueMapValue
private boolean openInNewTab;
@ValueMapValue
private String style; // primary, secondary, link
public String getTag() {
return link != null && !link.isEmpty() ? "a" : "button";
}
public String getHref() {
return "a".equals(getTag()) ? link : null;
}
public String getType() {
return "button".equals(getTag()) ? "button" : null;
}
public String getClasses() {
String baseClass = "btn";
String styleClass = "btn-" + (style != null ? style : "primary");
return baseClass + " " + styleClass;
}
public String getTarget() {
return openInNewTab && "a".equals(getTag()) ? "_blank" : null;
}
public String getRel() {
return openInNewTab && "a".equals(getTag()) ? "noopener noreferrer" : null;
}
public String getText() {
return text;
}
}Output (link with new tab):
<a href="/page" class="btn btn-primary" target="_blank" rel="noopener noreferrer">
Learn More
</a>Output (button without link):
<button type="button" class="btn btn-primary">
Submit
</button>Best Practices
- Always provide a fallback:
${tag || 'div'} - Validate author input: Use whitelist in model
- Semantic HTML: Prefer semantic tags (article, section, nav)
- Heading levels: Maintain correct hierarchy (h1 → h2 → h3)
- Accessibility: Use appropriate tags for screen readers
- Don't overuse: Use only when necessary
- Testing: Test with various values, including null/empty
Tag Validation in Model
private static final Set<String> ALLOWED_TAGS = Set.of(
"div", "section", "article", "aside", "nav", "main"
);
public String getSafeTag() {
if (tag != null && ALLOWED_TAGS.contains(tag.toLowerCase())) {
return tag.toLowerCase();
}
return "div"; // safe fallback
}Common Mistakes
❌ Dynamic tag without fallback
<!-- RISKY - if properties.tag is null -->
<div data-sly-element="${properties.tag}">
<!-- CORRECT - with fallback -->
<div data-sly-element="${properties.tag || 'div'}">❌ Unsafe tag
<!-- BLOCKED - script not allowed -->
<div data-sly-element="script">
<!-- Use only whitelisted tags -->
<div data-sly-element="section">❌ Confusing with data-sly-attribute
<!-- WRONG - this changes attribute, not tag -->
<div data-sly-attribute.element="h2">
<!-- CORRECT -->
<div data-sly-element="h2">Practical Exercises
- Heading Component: Heading with selectable h1-h6 levels
- List Component: ol/ul list with customizable type
- Container Component: Choose between div/section/article/aside
- Button/Link: Switch between and
Next Lesson
In the next lesson we'll see data-sly-unwrap to remove wrapper elements while keeping only the content.
Lesson #11 of the HTL Tutorial series. ← Previous lesson | Next lesson →