Client Libraries
Client libraries (clientlibs) are AEM's mechanism for managing CSS and JavaScript delivery. They provide dependency management, aggregation, minification, proxy serving, and cache busting -- all configured declaratively in the repository.
Unlike raw static files, clientlibs give you:
- Categories -- logical names that group CSS/JS files together
- Dependencies -- declare load order between libraries
- Embedding -- inline one library's content into another to reduce HTTP requests
- Proxy serving -- serve files from
/etc.clientlibs/instead of/apps/(which is blocked on publish) - Minification -- built-in CSS/JS minification and GZip compression
- Cache busting -- content-hash URLs for long-term browser caching
Clientlib Folder Structure
A clientlib is a JCR node of type cq:ClientLibraryFolder. On the filesystem (in ui.apps), it looks like this:
clientlib-site/
.content.xml <-- Node definition: categories, dependencies, embed, allowProxy
js.txt <-- Manifest: lists JS files to include (in order)
css.txt <-- Manifest: lists CSS files to include (in order)
js/ <-- JavaScript source files
main.js
utils.js
css/ <-- CSS source files
styles.css
layout.css
resources/ <-- Static resources: fonts, images, SVGs
fonts/
myfont.woff2
icons/
logo.svg
The .content.xml definition
<?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"
allowProxy="{Boolean}true"
categories="[myproject.site]"/>
| Property | Type | Description |
|---|---|---|
jcr:primaryType | String | Must be cq:ClientLibraryFolder |
categories | String[] | One or more category names this clientlib belongs to |
allowProxy | Boolean | Serve via /etc.clientlibs/ proxy (always set to true) |
dependencies | String[] | Categories that must load before this one |
embed | String[] | Categories whose content is inlined into this one |
channels | String[] | Device channels: [desktop], [mobile], or both (rarely used) |
cssProcessor | String[] | CSS processing pipeline (e.g. ["default:none", "min:gcc"]) |
jsProcessor | String[] | JS processing pipeline (e.g. ["min:gcc"]) |
The manifest files (js.txt / css.txt)
These plain-text files list which source files to include and in what order. Use #base= to set the
base directory, so you don't repeat the path prefix:
#base=js
main.js
utils.js
#base=css
styles.css
layout.css
Rules:
- One filename per line
- Lines starting with
#are directives or comments #base=jssets the base directory tojs/-- all subsequent filenames are relative to it- Files are concatenated in the order listed
- Missing files cause a build warning (but not a hard failure)
If you omit #base=, AEM looks for files directly inside the clientlib folder root. This is a common
source of "file not found" errors when your files are inside subdirectories.
The resources folder
Files under resources/ are served as-is (fonts, images, SVGs) and are accessible at:
/etc.clientlibs/myproject/clientlibs/clientlib-site/resources/fonts/myfont.woff2
Reference them in CSS with relative paths:
@font-face {
font-family: 'MyFont';
src: url('resources/fonts/myfont.woff2') format('woff2');
}
Categories
Categories are the identity of a clientlib. They are logical names -- not file paths -- that you reference when loading CSS/JS in your templates.
Naming conventions
Use a dot-separated namespace pattern to avoid collisions:
myproject.site <-- Global site CSS/JS
myproject.dependencies <-- Third-party vendor libs
myproject.components.hero <-- Component-specific
myproject.components.articlecard <-- Component-specific
cq.authoring.dialog <-- AEM built-in: loaded in all Touch UI dialogs
cq.authoring.editor.hook <-- AEM built-in: loaded in page editor
Multiple categories
A single clientlib can belong to multiple categories:
categories="[myproject.site, myproject.base]"
Both myproject.site and myproject.base will resolve to this clientlib's files.
Category collisions
If two clientlib folders declare the same category, both are loaded and their files are concatenated. This can cause unexpected duplicate CSS/JS or ordering issues. Always namespace with your project prefix and check for collisions with the dump libs tool.
Dependencies and Embedding
Dependencies
The dependencies property declares that other categories must be loaded before this clientlib.
Each dependency is loaded as a separate HTTP request.
<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"
allowProxy="{Boolean}true"
categories="[myproject.site]"
dependencies="[myproject.dependencies, myproject.base]"/>
When myproject.site is loaded, AEM ensures myproject.dependencies and myproject.base are loaded first (as separate requests).
Embedding
The embed property inlines another clientlib's CSS/JS content into this one. The embedded library's
files become part of this library's output -- no extra HTTP request.
<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"
allowProxy="{Boolean}true"
categories="[myproject.site]"
embed="[myproject.components.hero, myproject.components.articlecard, myproject.components.footer]"/>
Now a single request to myproject.site serves the combined output of all four libraries.
Dependencies vs embedding
| Dependencies | Embedding | |
|---|---|---|
| HTTP requests | One per dependency (separate files) | All inlined into one file |
| Caching | Each dependency cached independently | Single combined cache entry |
| Use when | Shared vendor libraries (jQuery, etc.) that other clientlibs also depend on | Component-specific CSS/JS that should be bundled into the main site file |
| Risk | More HTTP requests | Larger single file; embedded lib can't be cached separately |
A common pattern: use dependencies for large shared vendor libraries (so they're cached once and reused),
and embed for your own component clientlibs (so they're bundled into a single site-wide file).
Channels
The channels property restricts a clientlib to specific device channels:
channels="[desktop]"
This is rarely used in modern AEM projects. Responsive design via CSS media queries is preferred.
The allowProxy Mechanism
The /apps/ path is blocked by Dispatcher on publish instances for security reasons. Without allowProxy,
your clientlib CSS/JS would 404 on publish.
Setting allowProxy="{Boolean}true" makes AEM serve the clientlib content from /etc.clientlibs/ instead:
Actual location: /apps/myproject/clientlibs/clientlib-site/css/styles.css
Proxy URL: /etc.clientlibs/myproject/clientlibs/clientlib-site/css/styles.css
The proxy URL mirrors the path structure under /apps/ but is served from the Dispatcher-friendly /etc.clientlibs/ prefix.
Always set allowProxy="{Boolean}true". Without it:
- On AEM as a Cloud Service: clientlibs will not load on publish
- On AEM 6.5: clientlibs load on author but 404 on publish through Dispatcher
- It is a hard requirement, not just a best practice
Loading Clientlibs in HTL
AEM provides a built-in HTL template for loading clientlibs. First, import it:
<sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html"/>
Then call one of three methods:
clientlib.all (CSS + JS together)
Outputs both <link> and <script> tags:
<sly data-sly-call="${clientlib.all @ categories='myproject.site'}"/>
clientlib.css (CSS only)
Outputs only <link> tags:
<sly data-sly-call="${clientlib.css @ categories='myproject.site'}"/>
clientlib.js (JS only)
Outputs only <script> tags:
<sly data-sly-call="${clientlib.js @ categories='myproject.site'}"/>
Loading multiple categories
Pass an array:
<sly data-sly-call="${clientlib.all @ categories=['myproject.dependencies', 'myproject.site']}"/>
Async and defer loading
For non-blocking JS loading, use the loading attribute:
<!-- Defer: execute after HTML parsing, maintain order -->
<sly data-sly-call="${clientlib.js @ categories='myproject.site', loading='defer'}"/>
<!-- Async: execute as soon as downloaded, no order guarantee -->
<sly data-sly-call="${clientlib.js @ categories='myproject.analytics', loading='async'}"/>
Optimal page template placement
For best performance, split CSS and JS loading:
<!DOCTYPE html>
<html>
<head>
<sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html"/>
<!-- CSS in the head: renders immediately, avoids FOUC -->
<sly data-sly-call="${clientlib.css @ categories='myproject.site'}"/>
</head>
<body>
<!-- Page content -->
<div class="page">
<sly data-sly-resource="${'root' @ resourceType='wcm/foundation/components/responsivegrid'}"/>
</div>
<!-- JS at the end of body: doesn't block rendering -->
<sly data-sly-call="${clientlib.js @ categories='myproject.site', loading='defer'}"/>
</body>
</html>
Conditional loading (author-only)
Load clientlibs only in author mode (e.g. for editor customizations):
<sly data-sly-test="${wcmmode.edit || wcmmode.preview}">
<sly data-sly-call="${clientlib.all @ categories='myproject.authoring'}"/>
</sly>
Minification and Aggregation
AEM's HTML Library Manager handles minification and aggregation at runtime.
OSGi configuration
{
"htmllibmanager.minify": true,
"htmllibmanager.debug": false,
"htmllibmanager.gzip": true,
"htmllibmanager.timing": false,
"htmllibmanager.maxDataUriSize": 0,
"htmllibmanager.maxage": 604800,
"htmllibmanager.enforceContentDispositionHeader": false
}
| Property | Description | Default |
|---|---|---|
htmllibmanager.minify | Enable CSS/JS minification | true on publish, false on author |
htmllibmanager.debug | Serve individual unminified files (like ?debugClientLibs=true permanently) | false |
htmllibmanager.gzip | Enable GZip compression of clientlib output | true |
htmllibmanager.timing | Log timing information for clientlib processing | false |
htmllibmanager.maxage | Cache-Control max-age in seconds (604800 = 7 days) | 604800 |
Aggregation behaviour
When minification is enabled, AEM concatenates all files in a category into a single output:
Separate files (debug mode):
/etc.clientlibs/myproject/clientlibs/clientlib-site/js/main.js
/etc.clientlibs/myproject/clientlibs/clientlib-site/js/utils.js
Aggregated (production):
/etc.clientlibs/myproject/clientlibs/clientlib-site.min.js
Custom processors
Override the default minifier per clientlib using cssProcessor and jsProcessor:
<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"
allowProxy="{Boolean}true"
categories="[myproject.site]"
cssProcessor="[default:none, min:none]"
jsProcessor="[default:none, min:none]"/>
Setting processors to none disables AEM's minification for this clientlib -- useful when your
ui.frontend build already produces minified output.
Versioning and Cache Busting
AEM generates versioned URLs with a content hash so browsers can cache aggressively:
/etc.clientlibs/myproject/clientlibs/clientlib-site.lc-abc123def456-lc.min.css
^^^^^^^^^^^^^^^^
content hash
When the clientlib content changes, the hash changes, and browsers fetch the new version. Combined with
a long max-age header, this gives you instant cache invalidation without stale content.
On the Dispatcher side, ensure your cache rules allow /etc.clientlibs/ paths and that flush agents
invalidate clientlib URLs when code deployments occur.
On AEM as a Cloud Service, clientlib versioning and Dispatcher invalidation are handled automatically
by the CDN. On AEM 6.5 / AMS, configure your Dispatcher and CDN cache rules to respect the
lc-*-lc hash pattern in the URL.
Debugging Clientlibs
Debug query parameter
Append ?debugClientLibs=true to any page URL to serve individual, unminified source files instead of the
aggregated bundle. This makes it easy to identify which file contains a problem:
http://localhost:4502/content/my-site/en/home.html?debugClientLibs=true
This parameter only works on author instances (or where the HTML Library Manager debug mode is not explicitly disabled). It does not work on publish through Dispatcher.
Dump libs tool
Navigate to /libs/granite/ui/content/dumplibs.html to inspect all registered clientlibs:
http://localhost:4502/libs/granite/ui/content/dumplibs.html
This tool shows:
- All categories and their clientlib folders
- Dependencies and embeds for each category
- Category collisions (multiple folders declaring the same category)
- Broken dependencies (references to categories that don't exist)
Rebuild clientlibs
If clientlib changes are not reflected after modifying files on a running instance, force a rebuild:
http://localhost:4502/libs/granite/ui/content/dumplibs.rebuild.html
Click Invalidate Caches and then Rebuild Libraries. This clears AEM's in-memory clientlib cache and recompiles all libraries.
Browser DevTools tips
- Network tab: filter by
clientliboretc.clientlibsto see all loaded libraries - Source maps: AEM does not generate source maps by default. If you need them, use the
ui.frontendbuild pipeline (webpack/Vite) which produces source maps automatically - Console errors: missing clientlibs fail silently (no 404 for the HTML tag, just missing CSS/JS).
Check the
dumplibstool if styles or scripts are not loading
Preprocessors
Built-in LESS compiler
AEM includes a LESS compiler that processes .less files at runtime. Configure it via the
cssProcessor property:
<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"
allowProxy="{Boolean}true"
categories="[myproject.site]"
cssProcessor="[default:less, min:gcc;compilationLevel=simple]"/>
Then reference .less files in css.txt:
#base=css
variables.less
styles.less
SCSS, TypeScript, and modern tooling
AEM does not include built-in support for SCSS, TypeScript, or PostCSS. For these, use the
ui.frontend module with a webpack or Vite build pipeline. The build output is plain CSS/JS that
AEM serves as a regular clientlib.
See the Custom Component Guide -- Frontend via ui.frontend and the Multi-Tenancy UI Frontend Themes page for detailed setup.
Common Patterns
Site-wide clientlib
Global CSS/JS loaded on every page via the page template. Contains base styles, typography, layout grid, and shared JavaScript utilities.
<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"
allowProxy="{Boolean}true"
categories="[myproject.site]"
embed="[myproject.components.header, myproject.components.footer, myproject.components.navigation]"/>
Loaded in the page template's <head> and <body> (see optimal placement).
Component-scoped clientlib
CSS/JS placed inside a component folder, loaded only when that component is rendered.
components/
articlecard/
clientlibs/
clientlib-site/
.content.xml <-- categories="[myproject.components.articlecard]"
css.txt
css/articlecard.css
Loaded in the component's HTL:
<sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html"/>
<sly data-sly-call="${clientlib.css @ categories='myproject.components.articlecard'}"/>
Or embedded into the site-wide clientlib so all component styles are bundled into one request.
Dialog clientlib
JavaScript that runs inside the Touch UI dialog (authoring only). Used for show/hide logic, custom validation, or dynamic field behaviour.
<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"
allowProxy="{Boolean}true"
categories="[cq.authoring.dialog]"/>
The cq.authoring.dialog category is loaded automatically by AEM when any component dialog opens.
Scope your JS to your component using specific CSS selectors.
See Component Dialogs -- Dialog Clientlib and Custom Component Guide -- Dialog Clientlib for full examples.
Editor clientlib
JavaScript that hooks into the page editor (e.g. custom layer or toolbar behaviour):
categories="[cq.authoring.editor.hook]"
Theme / brand clientlib
Override base styles per site or brand. Used in multi-tenancy setups where multiple sites share components but differ in visual identity.
<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"
allowProxy="{Boolean}true"
categories="[myproject.theme.brand-a]"
dependencies="[myproject.site]"/>
Load the theme clientlib after the base site clientlib so its CSS overrides take effect. For a complete multi-tenancy setup, see Multi-Tenancy UI Frontend Themes.
Best Practices
- Always set
allowProxy="{Boolean}true"-- mandatory for AEMaaCS, required for publish on all environments. - Namespace categories with your project prefix --
myproject.site, not justsite. Avoids collisions with other packages. - Keep categories small and purposeful -- one clientlib per concern (site base, each component, editor hooks). Combine them via
embedin a parent clientlib. - Use
embedfor production bundling -- inline component clientlibs into the site clientlib to reduce HTTP requests. - Use
dependenciesfor shared vendor libraries -- so they are cached once and reused across clientlibs. - Split CSS and JS loading -- CSS in the
<head>, JS at the end of<body>withdefer. Avoids render-blocking. - Disable AEM minification when using
ui.frontend-- setcssProcessorandjsProcessortononeif webpack/Vite already minifies. - Use the
ui.frontendmodule for SCSS, TypeScript, or complex builds -- AEM's built-in processing is limited to LESS and basic minification. - Test with
?debugClientLibs=trueduring development to verify individual files load correctly. - Check the dump libs tool after deployments to detect category collisions or broken dependencies.
- Never put sensitive data in clientlibs -- they are publicly accessible on publish. No API keys, tokens, or server-side logic.
- Use
resources/for fonts and images -- they are served through the proxy and benefit from cache headers.
Common Pitfalls
Missing #base= directive
main.js
utils.js
#base=js
main.js
utils.js
Without #base=js, AEM looks for main.js directly inside the clientlib folder root, not in the js/ subdirectory.
Typo in category name
Clientlib loading fails silently. If your CSS/JS is not appearing, double-check:
- The category name in
.content.xmlmatches exactly what you reference in HTL - There are no invisible characters or trailing spaces
- Use the dump libs tool to verify the category is registered
Circular dependencies
If clientlib A depends on B and B depends on A, AEM enters an infinite loop. The page will hang or time out. Detect this with the dump libs tool, which reports circular dependency chains.
Forgetting allowProxy
Author: ✅ /apps/myproject/clientlibs/clientlib-site.css loads fine
Publish: ❌ 404 -- /apps/ is blocked by Dispatcher
Always add allowProxy="{Boolean}true" and reference the proxy URL in Dispatcher configs.
Loading heavy JS in the head
<!-- WRONG: blocks page rendering until all JS is downloaded and executed -->
<head>
<sly data-sly-call="${clientlib.all @ categories='myproject.site'}"/>
</head>
<!-- CORRECT: CSS in head, JS deferred at body end -->
<head>
<sly data-sly-call="${clientlib.css @ categories='myproject.site'}"/>
</head>
<body>
<!-- page content -->
<sly data-sly-call="${clientlib.js @ categories='myproject.site', loading='defer'}"/>
</body>
Stale clientlibs on a running instance
After modifying clientlib source files on a running instance (via CRXDE or package install), the in-memory cache may serve stale content. Use the rebuild tool to force recompilation, or restart the instance.
Embedded clientlib not updating
When you embed category B into category A, changes to B's files require A to be rebuilt as well. AEM does not always detect transitive changes. Use the rebuild tool after modifying embedded libraries.