All AEM as a Cloud Service skills: component development, Workflow model design, development, triggering, launchers, debugging, and triaging, Dispatcher config authoring, advisory, incident response, performance tuning, and security hardening, Rapid Development Environment (RDE) deployment, log inspection, snapshots, and troubleshooting via `aio aem rde`, Jackrabbit Oak query/index tuning (property, Lucene index definitions), and extracting/inventorying JCR queries (XPath, SQL2, QueryBuilder) across a codebase.
85%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
Low
Low-risk findings worth noting
Patterns for creating AEM client-side libraries (clientlibs) following Adobe best practices.
MUST CREATE a runtime clientlib with CSS for EVERY AEM component that has UI rendering.
When creating a component:
clientlib-{component-name}/ with CSSui.apps/src/main/content/jcr_root/apps/[project]/clientlibs/clientlib-{component-name}/
├── .content.xml
├── css.txt
├── css/
│ └── {component-name}.css
└── js/ (optional)
└── {component-name}.jsui.apps/src/main/content/jcr_root/apps/[project]/clientlibs/clientlib-{component-name}-dialog/
├── .content.xml
├── js.txt
└── js/
└── {component-name}-dialog.js<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
categories="[[project].components.{component-name}]"
allowProxy="{Boolean}true"/><?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:cq="http://www.day.com/jcr/cq/1.0"
xmlns:jcr="http://www.jcp.org/jcr/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
categories="[[project].{component-name}.dialog]"
dependencies="[cq.authoring.dialog]"/>#base=css
{component-name}.css#base=js
{component-name}.js/**
* Component: {component-name}
*/
/* Base component styles */
.cmp-{component-name} {
display: block;
position: relative;
}
/* Element: Title */
.cmp-{component-name}__title {
margin-bottom: 1rem;
font-size: 1.5rem;
}
/* Element: Content */
.cmp-{component-name}__content {
padding: 1rem;
}
/* Element: List */
.cmp-{component-name}__list {
list-style: none;
padding: 0;
margin: 0;
}
.cmp-{component-name}__item {
margin-bottom: 0.5rem;
}
/* Element: Link */
.cmp-{component-name}__link {
text-decoration: none;
color: inherit;
transition: all 0.3s ease;
}
.cmp-{component-name}__link:hover,
.cmp-{component-name}__link:focus {
text-decoration: underline;
outline: 2px solid currentColor;
outline-offset: 2px;
}
/* Modifier: Empty state */
.cmp-{component-name}--empty {
border: 2px dashed #ccc;
padding: 1rem;
text-align: center;
color: #666;
}
/* Responsive: Tablet */
@media (min-width: 768px) {
.cmp-{component-name} {
padding: 1.5rem;
}
.cmp-{component-name}__title {
font-size: 2rem;
}
}
/* Responsive: Desktop */
@media (min-width: 1024px) {
.cmp-{component-name} {
padding: 2rem;
}
}.cmp-{component} - Block (component root)
.cmp-{component}__{element} - Element (child)
.cmp-{component}--{modifier} - Modifier (variant)Every component CSS MUST include:
.cmp-{component-name})<sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html">
<sly data-sly-call="${clientlib.css @ categories='[project].components.{component-name}'}"/>
</sly>(function() {
'use strict';
window.MYSITE = window.MYSITE || {};
window.MYSITE.Components = window.MYSITE.Components || {};
window.MYSITE.Components.ComponentName = {
selectors: {
self: '[data-cmp-is="componentname"]',
button: '.cmp-componentname__button'
},
init: function(element) {
if (!element) return;
if (element.dataset.cmpInitialized) return;
element.dataset.cmpInitialized = 'true';
this.setupEventListeners(element);
},
setupEventListeners: function(element) {
var button = element.querySelector(this.selectors.button);
if (button) {
button.addEventListener('click', this.handleClick.bind(this, element));
}
},
handleClick: function(element, event) {
event.preventDefault();
// Handle click
}
};
// Auto-initialize
document.addEventListener('DOMContentLoaded', function() {
var components = document.querySelectorAll(
window.MYSITE.Components.ComponentName.selectors.self
);
components.forEach(function(element) {
window.MYSITE.Components.ComponentName.init(element);
});
});
})();CRITICAL: Coral UI components render asynchronously. You MUST use
foundation-contentloaded(notdialog-ready) and the Foundation Field API for reliable field manipulation.
/**
* Dialog clientlib for conditional field validation
* Category: [project].{component-name}.dialog
*
* IMPORTANT:
* - Use "foundation-contentloaded" event (fires AFTER Coral UI components are rendered)
* - Use adaptTo("foundation-field").setDisabled() for proper Granite UI field control
* - Apply granite:class to the field node in dialog XML (see granite:class placement guide below)
*/
(function(document, $) {
"use strict";
/**
* Selector for the controlling field (e.g., link text input)
* NOTE: granite:class must be on the textfield node in dialog XML
*/
var LINK_TEXT_SELECTOR = ".cmp-componentname__link-text";
/**
* Selector for the dependent field (e.g., link URL pathfield)
*/
var LINK_URL_SELECTOR = ".cmp-componentname__link-url";
/**
* Toggle the disabled state of dependent field based on controlling field value
*/
function toggleDependentField($controlField, $dependentField) {
var controlValue = $controlField.val();
if (controlValue && controlValue.trim().length > 0) {
// Enable the dependent field
$dependentField.prop("disabled", false);
$dependentField.adaptTo("foundation-field").setDisabled(false);
} else {
// Disable the dependent field
$dependentField.prop("disabled", true);
$dependentField.adaptTo("foundation-field").setDisabled(true);
}
}
/**
* Initialize dialog functionality when dialog content is fully loaded
* NOTE: "foundation-contentloaded" fires AFTER Coral UI components are rendered,
* unlike "dialog-ready" which may fire before inputs exist in the DOM
*/
$(document).on("foundation-contentloaded", function(e) {
var $dialog = $(e.target);
// Find all multifield items in the dialog
$dialog.find("coral-multifield-item").each(function() {
var $multifieldItem = $(this);
var $controlField = $multifieldItem.find(LINK_TEXT_SELECTOR);
var $dependentField = $multifieldItem.find(LINK_URL_SELECTOR);
if ($controlField.length && $dependentField.length) {
// Set initial state
toggleDependentField($controlField, $dependentField);
// Listen for changes on controlling field
$controlField.on("input change", function() {
toggleDependentField($controlField, $dependentField);
});
}
});
});
/**
* Handle multifield add event - initialize new items
*/
$(document).on("coral-collection:add", function(e) {
var $addedItem = $(e.target);
// Check if this is a multifield item
if ($addedItem.is("coral-multifield-item")) {
var $controlField = $addedItem.find(LINK_TEXT_SELECTOR);
var $dependentField = $addedItem.find(LINK_URL_SELECTOR);
if ($controlField.length && $dependentField.length) {
// Set initial state for new item
toggleDependentField($controlField, $dependentField);
// Listen for changes on controlling field
$controlField.on("input change", function() {
toggleDependentField($controlField, $dependentField);
});
}
}
});
})(document, Granite.$);foundation-contentloaded event (NOT dialog-ready) - ensures Coral UI is fully renderedadaptTo("foundation-field").setDisabled() for proper Granite UI field enable/disableGranite.$ for jQuery in AEM dialogscoral-collection:add for multifield support.cmp-componentname__field-name)granite:class to the field node in dialog XML (see placement guide below)granite:class Placement GuideAlways apply granite:class on the dialog XML node for the field. Where the CSS class ends up in the rendered HTML depends on the component type:
granite:class directly on the field node. Granite UI renders the class on the input element itself.granite:class on the field node. Granite UI propagates it to the rendered input/wrapper — your JS selector will find it on the coral component wrapper.granite:class on the multifield node itself, NOT on child field nodes. Child fields inside a multifield are duplicated per item, so target the multifield container.granite:class on the wrapper node that should be shown/hidden.granite:class Placement by Component| Component | sling:resourceType | Where to put granite:class | Rendered on |
|---|---|---|---|
| Textfield | .../form/textfield | Field node | <input> element |
| Textarea | .../form/textarea | Field node | <textarea> element |
| Checkbox | .../form/checkbox | Field node | <coral-checkbox> wrapper |
| Numberfield | .../form/numberfield | Field node | <coral-numberinput> wrapper |
| Pathfield | .../form/pathfield | Field node | <coral-pathfield> wrapper |
| Select | .../form/select | Field node | <coral-select> wrapper |
| Multifield | .../form/multifield | Multifield node (not children) | <coral-multifield> wrapper |
| Container | .../container | Container/wrapper node | <div> wrapper |
Tip: When in doubt, inspect the rendered dialog HTML in browser DevTools to verify where the class appears. Right-click the field in the dialog → Inspect Element, and search for your class name.
Correct granite:class placement on field nodes:
<!-- Simple field: granite:class goes on the field node -->
<linkText
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Link Text"
name="./linkText"
granite:class="cmp-componentname__link-text"/>
<!-- Complex field: granite:class also goes on the field node (Granite propagates it) -->
<linkURL
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/pathfield"
fieldLabel="Link URL"
name="./linkURL"
granite:class="cmp-componentname__link-url"/>
<!-- Multifield: granite:class goes on the MULTIFIELD node, not child fields -->
<links
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/multifield"
fieldLabel="Links"
composite="{Boolean}true"
granite:class="cmp-componentname__links-multifield">
<field jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container">
<!-- Child fields inside multifield do NOT get granite:class -->
<linkText
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Link Text"
name="./linkText"/>
</field>
</links>
<!-- Show/hide target container -->
<advancedSettings
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/container"
granite:class="cmp-componentname__advanced-settings">
<!-- fields inside this container -->
</advancedSettings>| Type | Category Pattern | allowProxy | Loads On | Included Via |
|---|---|---|---|---|
| Component | [project].components.{name} | Yes | Author + Publish | HTL |
| Dialog | [project].{name}.dialog | No | Author dialog only | extraClientlibs |
If mockup/image provided:
If no mockup:
.tessl-plugin
skills
aem-rde
aem-workflow
code-assessment
content-distribution
create-component
assets
references
dispatcher
incident-response
references
dispatcher-foundation
performance-tuning
references
dispatcher-foundation
security-hardening
references
dispatcher-foundation
technical-advisory
references
dispatcher-foundation
workflow-orchestrator
references
dispatcher-foundation
ensure-agents-md
references
migration
references
dispatcher
legacy-ui
template-modernization
validation
scripts
dispatcher-tool
validate-migration