Adobe AEM

AEM Core Components & the Style System: The Complete Guide

31 min read

A deep, practical guide to AEM Core Components — the component catalog, component versioning, the proxy component pattern, editable templates and policies, the Style System (cq:styleGroups, BEM, combinable styles), extending components with Sling Model delegation, dialog merging and HTL overrides, the Adobe Client Data Layer, accessibility, upgrades, and how delivery differs between AEM as a Cloud Service and AEM 6.5. Includes code, a cheat sheet, best practices, and do's & don'ts.

AEMCore ComponentsStyle SystemSling ModelsHTLTemplates
AEM Core Components & the Style System: The Complete Guide

Almost every modern AEM Sites project is built on the Core Components. Title, Text, Image, Teaser, Container, Navigation, the Page component itself: on a typical project, most of what authors drop onto a page is a Core Component with a thin layer of project code on top. Used well, they give you accessible, analytics-ready, JSON-exportable components that Adobe maintains for you. Used badly, with overlays, forked HTL, and content that points straight at Adobe's resource types, they turn every upgrade into a migration project.

This guide covers the parts you actually need to get right. It explains what Core Components are and how they're versioned, why the proxy component pattern matters, how editable templates and policies configure them, how the Style System gets a CSS class from a policy onto the page, and the supported ways to extend them: dialog merging, Sling Model delegation, and HTL overrides. It also covers the data layer, accessibility, upgrades, and how delivery differs between AEM as a Cloud Service and AEM 6.5. A cheat sheet, best practices, and do's & don'ts close it out.

This post goes deeper than the Core Components section of the Component Development guide, which covers dialogs, Sling Models, and HTL in general. The HTL cheat sheet and Annotations reference are useful companions. For client libraries and the front-end build, see the Frontend Integration guide. If you want boilerplate for a new component, the AEM Component Generator produces the HTL, dialog, and Sling Model for you.

What Core Components are (and why they exist)

Core Components are a set of standardized WCM components that Adobe develops in the open at github.com/adobe/aem-core-wcm-components under the Apache 2.0 license. They replaced the old JSP-based foundation components, and they're built the way Adobe recommends you build your own components:

  • Logic in Sling Models (Java interfaces in com.adobe.cq.wcm.core.components.models, with private implementations).
  • Markup in HTL, which escapes output for XSS protection by default.
  • Configuration through policies, so template authors decide which features page authors get.
  • BEM-style CSS classes (cmp-title, cmp-title__text) and Style System support.
  • JSON export through the Sling Model Exporter, for headless and SPA use.
  • Data layer integration with the Adobe Client Data Layer.

The release line at the time of writing is 2.32.x (2.32.8 shipped in September 2026). The repository is a Maven reactor, and it ships as a few artifacts you'll see in project POMs:

ArtifactWhat it contains
core.wcm.components.coreThe OSGi bundle: model interfaces, implementations, servlets
core.wcm.components.contentThe components themselves (HTL, dialogs, clientlibs) as a content package
core.wcm.components.configOSGi configurations the components depend on
core.wcm.components.allA container package bundling the above

The one hard requirement is editable templates. Core Components don't support static templates or the Classic UI. If you're migrating an older estate, Adobe points to the community-maintained AEM Modernize Tools, which convert static templates, design configurations, and foundation components.

The component catalog

The Core Components README lists 30 components in four categories. The version shown is the latest major version in the repository today, which is the one a new proxy should point at.

CategoryComponentLatestResource type
TemplatePagev3core/wcm/components/page/v3/page
TemplateNavigationv2core/wcm/components/navigation/v2/navigation
TemplateLanguage Navigationv2core/wcm/components/languagenavigation/v2/languagenavigation
TemplateBreadcrumbv3core/wcm/components/breadcrumb/v3/breadcrumb
TemplateQuick Searchv3core/wcm/components/search/v3/search
TemplateContent AI Searchv1core/wcm/components/contentaisearch/v1/contentaisearch
TemplateTable of Contentsv1core/wcm/components/tableofcontents/v1/tableofcontents
AuthoringTitlev3core/wcm/components/title/v3/title
AuthoringTextv2core/wcm/components/text/v2/text
AuthoringImagev3core/wcm/components/image/v3/image
AuthoringButtonv2core/wcm/components/button/v2/button
AuthoringTeaserv2core/wcm/components/teaser/v2/teaser
AuthoringListv4core/wcm/components/list/v4/list
AuthoringDownloadv2core/wcm/components/download/v2/download
AuthoringPDF Viewerv1core/wcm/components/pdfviewer/v1/pdfviewer
AuthoringEmbedv2core/wcm/components/embed/v2/embed
AuthoringProgress Barv1core/wcm/components/progressbar/v1/progressbar
AuthoringSeparatorv1core/wcm/components/separator/v1/separator
AuthoringExperience Fragmentv2core/wcm/components/experiencefragment/v2/experiencefragment
AuthoringContent Fragmentv1core/wcm/components/contentfragment/v1/contentfragment
AuthoringContent Fragment Listv2core/wcm/components/contentfragmentlist/v2/contentfragmentlist
ContainerContainerv1core/wcm/components/container/v1/container
ContainerCarouselv1core/wcm/components/carousel/v1/carousel
ContainerTabsv1core/wcm/components/tabs/v1/tabs
ContainerAccordionv1core/wcm/components/accordion/v1/accordion
FormForm Containerv2core/wcm/components/form/container/v2/container
FormForm Textv2core/wcm/components/form/text/v2/text
FormForm Optionsv2core/wcm/components/form/options/v2/options
FormForm Hiddenv2core/wcm/components/form/hidden/v2/hidden
FormForm Buttonv2core/wcm/components/form/button/v2/button

The Social Media Sharing component (sharing/v1) is still in the repository but has been flagged cq:deprecated since Core Components 2.18.0 with the reason "Should not be used in new projects". Leave it out of new builds.

Note: The Content Fragment and Experience Fragment components are covered in depth in the Content Fragments & Experience Fragments guide. This post focuses on the mechanics every Core Component shares.

Versioning: release versions vs component versions

Core Components carry two independent version numbers, and mixing them up causes a lot of confusion.

  • The release version (for example 2.32.8) is the Maven version of the whole library. It follows semantic versioning, and on AEM 6.5 it's what you bump in your POM.
  • The component version is the v1, v2, or v3 baked into the resource type path, such as core/wcm/components/title/v3/title. Client library categories follow the same scheme (core.wcm.components.image.v3).

A single release ships several component versions side by side (2.32.x contains Title v1, v2, and v3). Adobe only introduces a new component major version for backward-incompatible changes. According to the Core Components guidelines, that means incompatible changes to any of the following:

  • Sling Models (following semantic versioning)
  • HTL scripts and templates
  • HTML markup and CSS selectors
  • the JSON representation
  • dialogs

The practical consequence: a release upgrade within the same component version shouldn't break your markup or your customizations. A component major upgrade (Title v2 to v3) can, which is why you opt into it deliberately, one component at a time.

Each implementation (for example ...internal.models.v1.TitleImpl) binds itself to its resource types, while the HTL adapts to the interface:

<div data-sly-use.title="com.adobe.cq.wcm.core.components.models.Title"
     data-cmp-data-layer="${title.data.json}"
     id="${title.id}"
     class="cmp-title">
    <h1 class="cmp-title__text" data-sly-element="${title.type}">${title.text}</h1>
</div>

Sling Models picks the implementation registered for the current resource type. Adobe calls this double binding the model interface pattern, and it's what lets you swap in your own implementation later without touching the HTL.

The proxy component pattern

Here's the rule the whole system depends on: content must never reference a Core Component's resource type directly. Instead, you create a site-specific proxy component in /apps that inherits from the Core Component through sling:resourceSuperType, and authors only ever use the proxy.

<?xml version="1.0" encoding="UTF-8"?>
<!-- /apps/mysite/components/title/.content.xml -->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
          xmlns:cq="http://www.day.com/jcr/cq/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="cq:Component"
    jcr:title="Title"
    jcr:description="Section heading"
    sling:resourceSuperType="core/wcm/components/title/v3/title"
    componentGroup="My Site - Content"/>

That's a complete, working component. Sling resolves scripts, dialogs, and models by walking up the super-type chain, so an empty proxy behaves exactly like the Core Component. A few details are worth understanding.

Why the content must not contain a version number. Each content node stores its sling:resourceType. If a page's title node says core/wcm/components/title/v2/title, moving to v3 means rewriting that property on every title node in the repository. If it says mysite/components/title, moving to v3 is a one-line change to the proxy's sling:resourceSuperType. Adobe's guidance puts it bluntly: a content resource's sling:resourceType should never contain a version number.

Why you use relative resource types, never /libs or /apps paths. sling:resourceSuperType should be the relative path core/wcm/components/.... Sling resolves it against its search paths (/apps, then /libs). That matters because the Core Components live in different places on the two platforms: under /libs/core/wcm/components on AEM as a Cloud Service, and under /apps/core/wcm/components when you install them yourself on AEM 6.5. A relative type works on both, but a hardcoded absolute path breaks as soon as the code moves between them.

Why authors can't see Core Components directly. Every Core Component sits in a hidden component group, .core-wcm or .core-wcm-form. A group name that starts with a dot is hidden from the editor's component browser, so the proxy's componentGroup is what authors actually see.

Some Core Components delegate to another component internally, and the proxy is where you redirect that. The Teaser, for instance, renders its image through an image component named by the imageDelegate property. The AEM Project Archetype sets it to the project's own Image proxy:

<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
          xmlns:cq="http://www.day.com/jcr/cq/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="cq:Component"
    jcr:title="Teaser"
    sling:resourceSuperType="core/wcm/components/teaser/v2/teaser"
    componentGroup="My Site - Content"
    imageDelegate="mysite/components/image"/>

Important: Adobe's Cloud Service docs are explicit that even though the Core Components are in /libs, you should not overlay /apps/core/wcm/components/.... Anything you'd customize belongs in your proxy. An overlay silently replaces Adobe's file for every site on the instance and gets out of step with each automatic update.

Editable templates and policies

Core Components are configured through editable templates, which live under /conf/<site>/settings/wcm/templates. Each template has three parts:

  • structure: components forced onto every page (header, footer, the root container), which authors can't delete unless they're unlocked.
  • initial: content copied into a new page when it's created.
  • policies: a mapping from each component position to a content policy.

A policy is a component's pre-configuration: what a template author sets in the component's design dialog. Policies live under /conf/<site>/settings/wcm/policies, keyed by component resource type, so the same policy can be reused across templates. The template's policies node points at them with cq:policy:

<!-- /conf/mysite/settings/wcm/templates/article/policies/.content.xml (excerpt) -->
<jcr:content
    jcr:primaryType="nt:unstructured"
    sling:resourceType="wcm/core/components/policies/mappings">
    <root
        jcr:primaryType="nt:unstructured"
        cq:policy="mysite/components/container/policy_root"
        sling:resourceType="wcm/core/components/policies/mapping">
        <container
            jcr:primaryType="nt:unstructured"
            cq:policy="mysite/components/container/policy_main"
            sling:resourceType="wcm/core/components/policies/mapping"/>
    </root>
</jcr:content>

Policies control a surprising amount of behavior. For the Title v3 component, the policy's type property sets the default heading level and linkDisabled turns off linking. The Image v3 policy controls lazy loading (disableLazyLoading), Dynamic Media features (enableDmFeatures), and delivery through Cloud Service's Asset Delivery system (enableAssetDelivery). The container policy decides which components are allowed inside it. This is why Core Components need so few dialog customizations: the "variant" logic that used to require a new component is usually a policy toggle.

Allowing components is the final step of any Core Components setup. In the Template Editor, select the layout container, open its policy, and tick your proxy components (they appear under the componentGroup you gave them).

Tip: Treat /conf templates and policies as code. Build them in the Template Editor on a local instance, then pull them into ui.content and deploy them through your pipeline. Hand-editing policies directly on production author is how environments drift apart.

The Style System

The Style System lets a template author define named CSS classes in a component's policy, and lets a page author pick them from a Styles (paintbrush) menu on the component toolbar. It's how you give authors "Dark", "Centered", or "Hero" variants of a component without building a new component or adding a dialog field for each one.

The workflow splits cleanly: front-end developers write the CSS (and optional JS) for each variant, the AEM developer ships it in a client library and adds the class names to policies, and authors pick them. The CSS itself can be built and tested entirely outside AEM.

Note: The Style System only applies to pages created with the Page Editor. Pages built with the Universal Editor and served through Edge Delivery Services are styled through the project's code instead. See the Edge Delivery Services guide.

What's stored in the JCR

When a template author fills in the Styles tab of a policy, AEM writes a cq:styleGroups node under that policy. This excerpt follows the structure Adobe's WKND reference site uses for its Title policy:

<!-- under /conf/mysite/settings/wcm/policies/mysite/components/title/... -->
<policy_title_article
    jcr:primaryType="nt:unstructured"
    jcr:title="Article Title"
    sling:resourceType="wcm/core/components/policy/policy"
    type="h2">
    <cq:styleGroups jcr:primaryType="nt:unstructured">
        <item0
            jcr:primaryType="nt:unstructured"
            cq:styleGroupLabel="Decoration"
            cq:styleGroupMultiple="{Boolean}true">
            <cq:styles jcr:primaryType="nt:unstructured">
                <item0
                    jcr:primaryType="nt:unstructured"
                    cq:styleId="1568996484405"
                    cq:styleLabel="Underline"
                    cq:styleClasses="cmp-title--underline"/>
                <item1
                    jcr:primaryType="nt:unstructured"
                    cq:styleId="1570766394721"
                    cq:styleLabel="Mini spacing"
                    cq:styleClasses="cmp-title--minispacing"/>
            </cq:styles>
        </item0>
        <item1
            jcr:primaryType="nt:unstructured"
            cq:styleGroupLabel="Color">
            <cq:styles jcr:primaryType="nt:unstructured">
                <item0
                    jcr:primaryType="nt:unstructured"
                    cq:styleId="1568996420379"
                    cq:styleLabel="Black"
                    cq:styleClasses="cmp-title--black"/>
                <item1
                    jcr:primaryType="nt:unstructured"
                    cq:styleId="1568996427017"
                    cq:styleLabel="White"
                    cq:styleClasses="cmp-title--white"/>
            </cq:styles>
        </item1>
    </cq:styleGroups>
</policy_title_article>

The properties map directly to the fields in the policy dialog:

PropertyWhereMeaning
cq:styleGroupLabelgroupGroup name shown in the Styles menu
cq:styleGroupMultiplegroup"Styles can be combined": allow several styles from this group at once
cq:styleLabelstyleAuthor-facing name of the style
cq:styleClassesstyleThe CSS class(es) actually applied
cq:styleIdstyleStable ID that content references
cq:styleElementstyleOptional wrapper element name for this style
cq:styleDefaultClassespolicyClasses applied when the author picks nothing
cq:styleDefaultElementpolicyDefault wrapper element name

When an author picks a style, the content doesn't store the class name. It stores the style's ID in a cq:styleIds string array on the component's node. That indirection is useful: rename a class in the policy and every component using that style picks up the new class. The trade-off is that deleting a style from a policy (or recreating it with a new ID) quietly orphans every component that referenced the old ID. The content still holds the ID, but nothing is applied anymore.

Where the class lands

AEM wraps every editable component in a decoration element, and the Style System puts the selected classes on that wrapper, not on the component's own root element. For a Title inside a layout container, the rendered markup looks roughly like this:

<div class="title cmp-title--underline cmp-title--black aem-GridColumn aem-GridColumn--default--12">
    <div class="cmp-title">
        <h2 class="cmp-title__text">Our story</h2>
    </div>
</div>

The wrapper also carries a class named after the component (title) and, inside a responsive grid, the layout classes. The component's own markup starts at .cmp-title. This is why the component developer "doesn't need to do anything" for the Style System to work: the classes are applied outside the component's HTL entirely.

It also explains the one CSS rule you must follow. Style classes are ancestors of the component's BEM block, so style selectors are written as descendant selectors:

// The style class sits on the wrapper, so target the block/elements inside it.
.cmp-title--underline {
  .cmp-title__text::after {
    content: "";
    display: block;
    width: 84px;
    padding-top: 8px;
    border-bottom: 2px solid $brand-primary;
  }
}

.cmp-title--white .cmp-title__text {
  color: $text-color-inverse;
}

Writing .cmp-title.cmp-title--white (both classes on the same element) is the classic mistake. It never matches, because the two classes are on different elements.

The wrapper element name can change too. A component can declare the elements it allows with cq:styleElements on its cq:Component node (for example [div,section,aside]), and the template author then picks one per style. That's how a generic Container becomes a main or an aside. When several sources set the element name, Adobe documents this priority: a decorationTagName passed in HTL wins. Next comes the first active style in the policy's order, and then the component's cq:htmlTag / cq:tagName as a fallback. Avoid setting element names on styles that can be combined.

BEM naming and combining styles

Core Components follow BEM conventions: the block is cmp-<name>, elements are cmp-<name>__<element>, and modifiers are cmp-<name>--<modifier>. Each component's README documents its exact BEM structure. Name your Style System classes as modifiers of the block (cmp-teaser--hero, cmp-teaser--dark) and you get self-documenting, collision-free CSS.

Design your style groups around the combination rules:

  • Put mutually exclusive options (color themes, sizes) in a group without "Styles can be combined", so the menu behaves like radio buttons.
  • Put independent options (underline, extra spacing, align right) in a group with it enabled, so they behave like checkboxes.
  • Make sure combinable styles don't fight over the same CSS properties. If "Hero" and "Compact" both set padding, their order in the stylesheet decides the winner, and authors will call it a bug.

Enabling the Style System on your own components

Core Components v2 and later support the Style System out of the box, and so does any proxy of them. For a custom component, you add the Styles tab to its design dialog by including Adobe's tab:

<!-- _cq_design_dialog/.content.xml, inside content/items/tabs/items -->
<styletab
    jcr:primaryType="nt:unstructured"
    sling:resourceType="granite/ui/components/coral/foundation/include"
    path="/mnt/overlay/cq/gui/components/authoring/dialog/style/tab_design/styletab"/>

There's an optional edit dialog equivalent (.../style/tab_edit/styletab) that gives authors a Styles tab inside the component dialog as an alternative to the toolbar menu. It's not enabled by default, and it isn't required for the Style System to work.

Reading applied styles in Java

Occasionally logic needs to know which style is active (to pick an image rendition for a "hero" style, say). Don't parse cq:styleIds yourself. The Core Components' Component interface exposes getAppliedCssClasses(), which returns the author-selected classes as a space-delimited string. The JSON exporter emits the same value as appliedCssClassNames, which is how SPA front ends apply Style System classes. Under the hood it adapts the resource to AEM's com.adobe.cq.wcm.style.ComponentStyleInfo, which you can also use directly in a custom model:

ComponentStyleInfo styleInfo = resource.adaptTo(ComponentStyleInfo.class);
String classes = styleInfo != null ? styleInfo.getAppliedCssClasses() : null;
boolean isHero = classes != null && classes.contains("cmp-teaser--hero");

Extending Core Components

Adobe's customization guide describes a ladder of techniques. Start at the top and only go further down when the level above really can't do what you need. Each step down adds upgrade risk.

NeedTechniqueUpgrade risk
Visual variationCSS + Style SystemNone
Turn features on/off per templatePolicy (design dialog)None
Hide, replace, or add dialog tabsSling Resource Merger on the proxy's dialogLow
Change or add business logicSling Model delegationLow to medium
Change the HTML structureCopy HTL into the proxyHighest

And one rule sits above the table: never modify Core Component code directly. That includes overlays of core/wcm/components. Adobe's support doesn't cover it, and it makes every update painful.

Customizing dialogs with the Sling Resource Merger

A proxy that defines its own _cq_dialog doesn't replace the Core dialog. The Sling Resource Merger merges the two along the super-type chain, so you only declare the differences. It gives you a small set of control properties:

PropertyEffect
sling:hideResourceHide an inherited node (e.g. a whole tab)
sling:hideChildrenHide listed inherited children (* hides all)
sling:hidePropertiesHide listed inherited properties
sling:orderBeforePlace this node before the named sibling

Adobe's guidance is specific about where to make changes. Replicate the dialog's node structure down to the tab level, and make your changes there: hide whole tabs and add new tabs. Don't hide, reorder, or inject individual fields below tab level, because Adobe may restructure a tab's internals in a minor release and your merged paths would stop matching.

Here's a Teaser proxy that adds a "Campaign" tab ahead of the Styles tab and hides the Links tab. The tab node names (actions, text, image, styletab) come from the Teaser v2 dialog in the repository:

<?xml version="1.0" encoding="UTF-8"?>
<!-- /apps/mysite/components/teaser/_cq_dialog/.content.xml -->
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
          xmlns:nt="http://www.jcp.org/jcr/nt/1.0"
    jcr:primaryType="nt:unstructured">
    <content jcr:primaryType="nt:unstructured">
        <items jcr:primaryType="nt:unstructured">
            <tabs jcr:primaryType="nt:unstructured">
                <items jcr:primaryType="nt:unstructured">
                    <actions
                        jcr:primaryType="nt:unstructured"
                        sling:hideResource="{Boolean}true"/>
                    <campaign
                        jcr:primaryType="nt:unstructured"
                        jcr:title="Campaign"
                        sling:orderBefore="styletab"
                        sling:resourceType="granite/ui/components/coral/foundation/container"
                        margin="{Boolean}true">
                        <items jcr:primaryType="nt:unstructured">
                            <campaignCode
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                fieldLabel="Campaign code"
                                fieldDescription="Tracking code shown as the pretitle when none is authored"
                                name="./campaignCode"/>
                        </items>
                    </campaign>
                </items>
            </tabs>
        </items>
    </content>
</jcr:root>

The Resource Merger works the same on 6.5 and Cloud Service. Adobe also recommends Granite UI hide conditions alongside the Resource Merger when a field should only appear in some contexts.

Extending logic with Sling Model delegation

The Core model implementations are in internal packages. They aren't exported, so you can't extend them, only their interfaces. Customization therefore uses delegation: your model implements the same interface, registers itself for your proxy's resource type, and gets the Core model injected to handle everything you don't override.

The injection is done with @Self @Via(type = ResourceSuperType.class). This Sling Models feature (added by SLING-6778) adapts the request again, but with the resource type swapped for the resource's super type, so you get exactly the model the Core Component would have used:

package com.mysite.core.models;

import java.util.List;

import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Exporter;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.injectorspecific.Self;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;
import org.apache.sling.models.annotations.via.ResourceSuperType;

import com.adobe.cq.export.json.ComponentExporter;
import com.adobe.cq.export.json.ExporterConstants;
import com.adobe.cq.wcm.core.components.commons.link.Link;
import com.adobe.cq.wcm.core.components.models.ListItem;
import com.adobe.cq.wcm.core.components.models.Teaser;
import com.adobe.cq.wcm.core.components.models.datalayer.ComponentData;

@Model(
    adaptables = SlingHttpServletRequest.class,
    adapters = {Teaser.class, ComponentExporter.class},
    resourceType = CampaignTeaser.RESOURCE_TYPE,
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(
    name = ExporterConstants.SLING_MODEL_EXPORTER_NAME,
    extensions = ExporterConstants.SLING_MODEL_EXTENSION)
public class CampaignTeaser implements Teaser {

    static final String RESOURCE_TYPE = "mysite/components/teaser";

    /** The Core Teaser model, resolved through the proxy's sling:resourceSuperType. */
    @Self
    @Via(type = ResourceSuperType.class)
    private Teaser delegate;

    @ValueMapValue
    private String campaignCode;

    // --- The one behavior we change ---
    @Override
    public String getPretitle() {
        String pretitle = delegate.getPretitle();
        return StringUtils.isNotBlank(pretitle) ? pretitle : campaignCode;
    }

    // --- New API for our own HTL ---
    public String getCampaignCode() {
        return campaignCode;
    }

    // --- Everything else is delegated explicitly ---
    @Override public String getTitle() { return delegate.getTitle(); }
    @Override public String getDescription() { return delegate.getDescription(); }
    @Override public String getTitleType() { return delegate.getTitleType(); }
    @Override public boolean isTitleLinkHidden() { return delegate.isTitleLinkHidden(); }
    @Override public boolean isImageLinkHidden() { return delegate.isImageLinkHidden(); }
    @Override public boolean isActionsEnabled() { return delegate.isActionsEnabled(); }
    @Override public List<ListItem> getActions() { return delegate.getActions(); }
    @Override public Link getLink() { return delegate.getLink(); }
    @Override public Resource getImageResource() { return delegate.getImageResource(); }
    @Override public String getId() { return delegate.getId(); }
    @Override public ComponentData getData() { return delegate.getData(); }
    @Override public String getAppliedCssClasses() { return delegate.getAppliedCssClasses(); }

    @Override
    public String getExportedType() {
        // The delegate would report the Core super type; report our proxy instead.
        return RESOURCE_TYPE;
    }
}

Because the HTL uses data-sly-use.teaser="com.adobe.cq.wcm.core.components.models.Teaser" (the interface), Sling Models now resolves your implementation for mysite/components/teaser. The Core HTL renders your pretitle without being copied.

A few traps catch almost everyone the first time:

  • Every Core interface method is a default method. The defaults return null or false. If you forget to delegate a method, it compiles fine and silently returns null: the image vanishes, or the data layer entry disappears. Delegate every method the HTL or JSON uses, and re-check the interface when you upgrade, because new releases add new default methods.
  • getExportedType() on the delegate returns the Core super type. Override it if a SPA front end maps components by type.
  • The proxy must declare sling:resourceSuperType. @Via(type = ResourceSuperType.class) has nothing to resolve without it.
  • Unit test the model. AEM Mocks can load a Core Component model from the core.wcm.components.core test dependency. See the Unit Testing guide.

Overriding the HTL

When CSS can't achieve a design (you need an extra element, or a different order), copy the Core HTL file you need to change into the proxy under the same file name. Sling's script resolution finds the proxy's file first, before the super type's. For the Teaser, you'd copy teaser.html into /apps/mysite/components/teaser/ and edit it. The Teaser also splits its markup into small template files (pretitle.html, title.html, description.html, actions.html, image.html), so look at the structure before deciding how much to copy.

This is the most upgrade-sensitive customization, because your copy freezes that markup at the version you copied. When a release fixes an accessibility issue or adds a data layer attribute in that file, you don't get the fix. Keep a comment in the copied file recording the Core release and component version it came from, keep the BEM class names unchanged so the Style System CSS keeps working, and diff your copy against upstream on every upgrade.

<!--/* Copied from core/wcm/components/teaser/v2/teaser/teaser.html (Core Components 2.32.x).
       Change: campaign badge above the content. Re-diff on every Core upgrade. */-->
<div data-sly-use.teaser="com.adobe.cq.wcm.core.components.models.Teaser"
     data-sly-use.campaign="com.mysite.core.models.CampaignTeaser"
     ...>
    <span class="cmp-teaser__campaign" data-sly-test="${campaign.campaignCode}">${campaign.campaignCode}</span>
    ...
</div>

The Adobe Client Data Layer (briefly)

Since Core Components 2.9.0, the Adobe Client Data Layer (ACDL) is shipped with the Core Components as a client library. Projects generated from the AEM Project Archetype v24 and later have it switched on. It's controlled by a context-aware configuration, not an OSGi config:

/conf/mysite/sling:configs/com.adobe.cq.wcm.core.components.internal.DataLayerConfig
    enabled = true   (Boolean)

/content/mysite/jcr:content
    sling:configRef = /conf/mysite

When it's enabled, the body gets a data-cmp-data-layer-enabled attribute and the page pushes its data into window.adobeDataLayer. Supported components serialize their data into a data-cmp-data-layer attribute, and clickable elements carry data-cmp-clickable. The layer raises cmp:click on clicks, and cmp:show / cmp:hide when Accordion, Tabs, or Carousel panels change. Run adobeDataLayer.getState() in the browser console to inspect it.

Your own components can join in by exposing a getData() built with DataLayerBuilder and rendering data-cmp-data-layer="${model.data.json}". When you delegate, return the delegate's getData() (as the teaser above does) unless you deliberately want to change the payload. The full analytics and Target wiring is in the Analytics, Target & Data Layer guide.

Accessibility

Accessibility is one of the strongest reasons to use Core Components rather than rolling your own. The project README describes them as compliant with WCAG 2.1, with ARIA labels and keyboard navigation (Adobe's comparison table with the foundation components cites WCAG 2.0 AA). Concretely:

  • Tabs render role="tablist", role="tab", and role="tabpanel" with aria-controls / aria-labelledby wiring, plus an author-set accessibilityLabel. Carousel has an accessibilityLabel too.
  • Carousel autoplay can be paused, has pause/play buttons, and pauses automatically when the document is hidden.
  • Image v3 makes authors choose: provide alt text, inherit it from the DAM asset (altValueFromDAM), or mark the image decorative (isDecorative) so assistive technology ignores it.
  • Page v3 supports a "skip to main content" link driven by the mainContentSelector page property (the ID of the main content element).
  • Title and Teaser let template authors control heading levels, so the page outline stays logical.

The components are accessible building blocks, not an accessible page. Your CSS can still break focus outlines or contrast, a copied HTL file can drop an ARIA attribute, and authors can still pick the wrong heading level. Keep every aria-* and role attribute when you copy HTL, and include Style System variants in accessibility testing.

The Component Library and the Components console

Two tools make Core Components easier to work with day to day:

  • The Component Library at aemcomponents.dev shows every Core Component in its various configurations, with the rendered HTML and the JSON model output. It's the fastest way to see the markup you're styling and what a policy option changes.
  • The Components console (Tools → General → Components, at /libs/wcm/core/content/sites/components.html) lists every component installed on the instance, with its properties (title, group, super type), the policies defined for it and the templates using them, and live usage: the pages where the component appears. It answers "is anyone still using the old v1 proxy?" before you remove it.

How Core Components ship: Cloud Service vs AEM 6.5

This is where the two platforms differ most, and it changes your build.

AspectAEM as a Cloud ServiceAEM 6.5 / 6.5 LTS
Where they live/libs/core/wcm/components (part of the product)/apps/core/wcm/components (installed by you)
Which releaseAlways the latest; Adobe updates them with AEMWhatever version your all package embeds
Your POMNo Core Components content in the build; compile against aem-sdk-apiDepend on and embed core.wcm.components.core, .content, .config
UpgradingAutomatic; you retest after AEM updatesBump core.wcm.components.version and redeploy
Local devIncluded in the AEM SDK quickstartInstall via your all package (or Package Manager)

On AEM as a Cloud Service, there's no install step. The components ship in /libs and Adobe keeps them on the latest release. Adobe's docs state that if your project embeds the Core Components again under /apps, the build pipeline logs a warning and ignores the embedded copy, and that a future release will make it a build failure. Treat it as an error now and remove the embed. When you move a project from 6.5 to Cloud Service, Adobe's advice is simply to remove the Core Components dependency from your POM and depend on aem-sdk-api. Your proxies keep working, because they point at a versioned super type that exists in /libs. More in the Cloud Service migration guide.

On AEM 6.5, the Core Components aren't part of a production-mode quickstart, so you ship them yourself. The AEM Project Archetype does this by embedding the three Core artifacts into the all package under a vendor-packages folder:

<!-- all/pom.xml (AEM 6.5 project), inside the filevault-package-maven-plugin <embeddeds> -->
<embedded>
    <groupId>com.adobe.cq</groupId>
    <artifactId>core.wcm.components.content</artifactId>
    <type>zip</type>
    <target>/apps/mysite-vendor-packages/application/install</target>
</embedded>
<embedded>
    <groupId>com.adobe.cq</groupId>
    <artifactId>core.wcm.components.core</artifactId>
    <target>/apps/mysite-vendor-packages/application/install</target>
</embedded>
<embedded>
    <groupId>com.adobe.cq</groupId>
    <artifactId>core.wcm.components.config</artifactId>
    <type>zip</type>
    <target>/apps/mysite-vendor-packages/application/install</target>
</embedded>

Check the compatibility matrix in the Core Components README before choosing a release. At the time of writing, releases 2.28.x through 2.32.x support AEM 6.5 LTS (from GA) and AEM 6.5 from service pack 6.5.21.0, on Java 8, 11, 17, and 21. Releases up to 2.25.x target older 6.5 service packs. On Cloud Service the support is simply "continual".

Important: Because Cloud Service updates Core Components under you, your safety net is tests, not version pinning. Make sure your proxies, delegating models, and copied HTL are covered by unit tests and UI tests that run in the Cloud Manager pipeline. A new default interface method or a markup change in a minor release should fail a test, not reach production quietly.

Upgrading Core Components

Upgrades come in three kinds, and Adobe's customization guide sets different expectations for each:

  1. Upgrading AEM itself. This shouldn't affect Core Components or your customizations, as long as the component versions support the new AEM version and your code doesn't use deprecated or removed APIs.
  2. A new Core release, same component versions (for example 2.31 to 2.32). This shouldn't affect customizations built with the documented patterns (proxies, dialog merging above the field level, delegation). It's routine on 6.5 and automatic on Cloud Service.
  3. A new component major version (Title v2 to v3). Content structure stays compatible, but your customizations may need refactoring. Adobe publishes change logs per component version for this.

A safe playbook for a component major upgrade:

  1. Read the new version's README in the repository. Note BEM changes, dialog changes, removed policy options, and new interface methods.
  2. Point the proxy's sling:resourceSuperType at the new version on a branch.
  3. Re-check any delegating model: the Core model interface is shared across versions, but the new version's implementation may behave differently or use methods you never delegated.
  4. Diff any copied HTL against the new version's file and port your change onto the new markup (don't port the old markup forward).
  5. Update client library embeds (core.wcm.components.image.v2 becomes core.wcm.components.image.v3) and any CSS that relied on changed selectors.
  6. Revisit policies: new versions may add options (like Image v3's Asset Delivery option) or read existing ones differently.
  7. Test with real content. No content migration is needed, because content only references mysite/components/....

Tip: You don't have to upgrade every component at once. Each proxy chooses its own version, so you can move the Image to v3 this sprint and the List to v4 next quarter.

Cheat sheet

NeedWhere / what
Source and releasesgithub.com/adobe/aem-core-wcm-components (2.32.x line)
Resource type patterncore/wcm/components/<name>/v<N>/<name>
Proxy componentcq:Component in /apps/<site>/components with sling:resourceSuperType + componentGroup
Hidden Core groups.core-wcm, .core-wcm-form
Templates/conf/<site>/settings/wcm/templates
Policies/conf/<site>/settings/wcm/policies
Policy mappingcq:policy on wcm/core/components/policies/mapping nodes
Style definitionscq:styleGroups → cq:styles → cq:styleClasses / cq:styleId
Selected styles on contentcq:styleIds (String array of IDs)
Default style / elementcq:styleDefaultClasses, cq:styleDefaultElement on the policy
Allowed wrapper elementscq:styleElements on the cq:Component
Style tab (design dialog)include /mnt/overlay/cq/gui/components/authoring/dialog/style/tab_design/styletab
Applied classes in JavaComponent#getAppliedCssClasses() or ComponentStyleInfo
Hide inherited dialog nodesling:hideResource="{Boolean}true"
Reorder dialog tabsling:orderBefore="<sibling>"
Delegate to Core model@Self @Via(type = ResourceSuperType.class)
Data layer toggleCA config com.adobe.cq.wcm.core.components.internal.DataLayerConfig → enabled
Component Libraryaemcomponents.dev
Components consoleTools → General → Components
Cloud Service location/libs/core/wcm/components (don't embed, don't overlay)
AEM 6.5 location/apps/core/wcm/components (embed in all)

Best practices

  • ✅ Create a proxy for every Core Component you use, per site, and let content reference only proxies.
  • ✅ Use relative super types (core/wcm/components/...), never absolute /libs or /apps paths.
  • ✅ Point new proxies at the latest component version and upgrade old ones deliberately, one at a time.
  • ✅ Reach for CSS + Style System and policies before touching dialogs, models, or HTL.
  • ✅ Name style classes as BEM modifiers (cmp-<name>--<variant>) and write them as descendant selectors of the wrapper.
  • ✅ Split style groups into exclusive and combinable sets, and keep combinable styles from fighting over the same properties.
  • ✅ Customize logic with delegation, delegating every interface method explicitly.
  • ✅ Keep templates and policies in /conf under source control and deploy them through the pipeline.
  • ✅ On Cloud Service, back your customizations with automated tests, because Core updates arrive without a POM change.

Do's and Don'ts

Do

  • ✅ Do keep version numbers out of content. Only the proxy knows the version.
  • ✅ Do record the source release in any copied HTL and diff it on every upgrade.
  • ✅ Do override getExportedType() in delegating models used by SPA front ends.
  • ✅ Do include your Style System variants in accessibility testing.
  • ✅ Do remove the Core Components embed from your build when moving to Cloud Service.

Don't

  • ❌ Don't overlay or edit anything under core/wcm/components, in /libs or /apps.
  • ❌ Don't let authors use Core Components directly or unhide the .core-wcm groups.
  • ❌ Don't hide or reorder dialog fields below tab level. Hide and add whole tabs instead.
  • ❌ Don't write .cmp-title.cmp-title--dark. The style class is on the wrapper, not the block.
  • ❌ Don't delete or recreate styles in a live policy without checking which content references their cq:styleId.
  • ❌ Don't copy HTL when CSS, a policy, or a delegated model would do.
  • ❌ Don't create a new component for what's really a style or a policy option.

Wrapping up

Core Components are Adobe's reference implementation of how an AEM component should be built: versioned, open source, accessible, analytics-ready, and exportable to JSON. The system rests on a few ideas. Content references proxies, proxies reference versions, so upgrades are configuration changes rather than content migrations. Templates and policies decide what each component can do on each template. The Style System turns policy-defined classes into author-selectable variants that land on the component's wrapper, ready for BEM-named CSS. When you need more, go down the ladder in order: merge the dialog, delegate the model with @Via(type = ResourceSuperType.class), and copy HTL only as a last resort. Master that, and you'll spend your time on the parts of the site that are actually unique to it.

Continue with the Component Development guide for building fully custom components, the Sling guide for how resource types and super types resolve, the Frontend Integration guide for the client libraries that carry your Style System CSS, and the Cloud Service guide for how Core Components fit into the Cloud Service release cycle. When you're ready to scaffold, the AEM Component Generator gives you a head start.

Share this article

Discussion

By commenting you agree to the Privacy Policy. Guest comments are reviewed before they appear.

Loading discussion…

Subscribe to the Newsletter

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

Back to Blog