Skip to main content

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:

Annotated clientlib structure
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

clientlib-site/.content.xml
<?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]"/>
PropertyTypeDescription
jcr:primaryTypeStringMust be cq:ClientLibraryFolder
categoriesString[]One or more category names this clientlib belongs to
allowProxyBooleanServe via /etc.clientlibs/ proxy (always set to true)
dependenciesString[]Categories that must load before this one
embedString[]Categories whose content is inlined into this one
channelsString[]Device channels: [desktop], [mobile], or both (rarely used)
cssProcessorString[]CSS processing pipeline (e.g. ["default:none", "min:gcc"])
jsProcessorString[]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:

js.txt
#base=js
main.js
utils.js
css.txt
#base=css
styles.css
layout.css

Rules:

  • One filename per line
  • Lines starting with # are directives or comments
  • #base=js sets the base directory to js/ -- 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)
warning

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

DependenciesEmbedding
HTTP requestsOne per dependency (separate files)All inlined into one file
CachingEach dependency cached independentlySingle combined cache entry
Use whenShared vendor libraries (jQuery, etc.) that other clientlibs also depend onComponent-specific CSS/JS that should be bundled into the main site file
RiskMore HTTP requestsLarger single file; embedded lib can't be cached separately
tip

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.

danger

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:

page.html -- recommended placement
<!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

ui.config/.../config/com.adobe.granite.ui.clientlibs.impl.HtmlLibraryManagerImpl.cfg.json
{
"htmllibmanager.minify": true,
"htmllibmanager.debug": false,
"htmllibmanager.gzip": true,
"htmllibmanager.timing": false,
"htmllibmanager.maxDataUriSize": 0,
"htmllibmanager.maxage": 604800,
"htmllibmanager.enforceContentDispositionHeader": false
}
PropertyDescriptionDefault
htmllibmanager.minifyEnable CSS/JS minificationtrue on publish, false on author
htmllibmanager.debugServe individual unminified files (like ?debugClientLibs=true permanently)false
htmllibmanager.gzipEnable GZip compression of clientlib outputtrue
htmllibmanager.timingLog timing information for clientlib processingfalse
htmllibmanager.maxageCache-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.

tip

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
warning

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 clientlib or etc.clientlibs to see all loaded libraries
  • Source maps: AEM does not generate source maps by default. If you need them, use the ui.frontend build 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 dumplibs tool 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:

.content.xml with LESS support
<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:

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.

clientlibs/clientlib-site/.content.xml
<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.

clientlib-editor/.content.xml
<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.

clientlib-theme-brand-a/.content.xml
<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 just site. Avoids collisions with other packages.
  • Keep categories small and purposeful -- one clientlib per concern (site base, each component, editor hooks). Combine them via embed in a parent clientlib.
  • Use embed for production bundling -- inline component clientlibs into the site clientlib to reduce HTTP requests.
  • Use dependencies for 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> with defer. Avoids render-blocking.
  • Disable AEM minification when using ui.frontend -- set cssProcessor and jsProcessor to none if webpack/Vite already minifies.
  • Use the ui.frontend module for SCSS, TypeScript, or complex builds -- AEM's built-in processing is limited to LESS and basic minification.
  • Test with ?debugClientLibs=true during 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

WRONG -- files won't be found
main.js
utils.js
CORRECT
#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:

  1. The category name in .content.xml matches exactly what you reference in HTL
  2. There are no invisible characters or trailing spaces
  3. 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.

See also