For AI agents: the complete documentation index is available at https://nhanbe.id.vn/llms.txt, the full documentation bundle is available at https://nhanbe.id.vn/llms-full.txt, and this page is available as Markdown at https://nhanbe.id.vn/aem/advanced/core-components-and-style-system.md.

Core Components and the Style System

22 – Core Components and the Style System

Target: 3–4 years experienced AEM Developer Syllabus point covered: "What are Core Components? How do you extend them? What is the Style System and how does it differ from a variation dropdown? What is the Responsive Grid / Layout Container?" Project domain: a global energy technology company's marketing site.


Before we start — why this file matters more than most

File 02 already told you the honest modern answer to "how do you build a component": default to a Core Component, extend it through a proxy, and build custom only when nothing else fits. That answer is correct, and it is also exactly where most candidates stop — they can say the sentence, but they cannot survive the follow-up.

And there is always a follow-up, because Core Components is the topic where interviewers separate "has read the documentation" from "has lived with this on a real project." The documentation tells you Core Components exist and are versioned. It does not tell you what actually happens when your project sits on v1 of the Accordion and Adobe ships v2 with different HTL data attributes. It does not tell you why the Style System is centrally controlled while a dialog dropdown is not. It does not tell you why the Responsive Grid is the right tool on a landing page and the wrong tool inside a component that already has its own CSS grid.

So this file has one job: take the proxy pattern you already know from file 02, and go deeper — into version-upgrade mechanics, the delegation pattern for extending a Core Component's Java logic (not just its markup), the Style System's actual storage location, and the Responsive Grid's real mechanism. If file 02 got you through "would you use Core Components," this file gets you through "you're on v1, Adobe just released v3, walk me through the upgrade."

A note on the project domain

Same site as every other file in this repository — a global energy technology company selling transformers, HVDC systems, grid automation and power quality equipment to utilities and industrial customers, across roughly twenty country sites. Never name a real client in an interview; "a global energy technology company" is the right level of detail.

⚠️ Read this before using any story in this file

These stories are shapes, not scripts. If you say "we upgraded Core Components and something broke," expect four follow-up questions about exactly what broke and how you found it. Fill these shapes with something you can actually defend — a POC, a support incident you traced, or WKND tutorial work all count. A borrowed story that collapses under one follow-up does more damage than admitting you haven't done it yet.


1. Introduction

1.1 What Core Components actually are

Here is the definition worth giving first, because it is more precise than "Adobe's pre-built components":

Core Components are a separate, versioned open-source project — maintained and released by Adobe independently of the AEM product itself — that ships around thirty production-quality, accessible components covering the things almost every website needs.

That "independently of the AEM product itself" clause is the part people skip, and it is the part that makes the rest of this file make sense. Core Components are not baked into your AEM installation the way, say, the Sling Post Servlet is. They are a Maven dependency your project pulls in, versioned on their own release cadence (their version numbers do not track AEM's version numbers), and you upgrade them by bumping a dependency — the same way you would upgrade any other library. Adobe ships bug fixes, accessibility improvements and new features to Core Components on a schedule that has nothing to do with when your AEM instance itself gets patched.

That single fact answers a cluster of interview questions at once: why they are versioned per-component rather than per-release, why upgrading them is a project decision rather than something that happens to you, and why two projects on the same AEM version can be running completely different Core Components versions.

1.2 Why they exist — the problem before Core Components

Before Core Components, every AEM project built its own Text, Image, Teaser, Carousel and Accordion from scratch. That meant every project separately solved — or failed to solve — the same accessibility requirements, the same responsive image handling, the same edge cases in rich text sanitisation. Adobe's own reference implementations (WCM Foundation components, then the AEM WKND tutorial's demo components) showed the pattern, but did not give you a maintained, reusable library.

Core Components changed the default. Now the honest starting point on any new project is "does a Core Component already do this," not "let's build one." File 02 covered the decision tree; this file assumes you already agree with the conclusion and asks: given that you're using them, what do you actually need to know to run a real project on them?

1.3 What's actually in the box

Worth being able to name a representative set, because "which Core Components have you used" is a completely ordinary opening question, and a vague answer ("the usual ones") reads as not having actually built with them.

Page-structure and navigation: Page, Breadcrumb, Navigation, Language Navigation, Search.

Content: Title, Text, Image, Button.

Composite / marketing: Teaser, List, Carousel, Tabs, Accordion.

Layout / container: Container (the Layout Container — covered in depth in section 2.9), Experience Fragment, Content Fragment.

Utility: Embed, Separator, Download, PDF Viewer.

Forms: the Core Form components — Form Container, Text, Checkbox, Options, Button, and others for building AEM Forms-integrated or plain HTML forms.

You will not need every one of these on every project, but you should be able to say which ones your (real or adapted) project actually proxies, because that is exactly the kind of concrete detail that separates a credible answer from a recited list.

1.4 The versioning model — v1, v2, v3, and why it exists

Open the Core Components repository or /libs/core/wcm/components on your instance and you will see folders like this:

/apps/core/wcm/components/
    accordion/
        v1/
            accordion/
        v2/
            accordion/
    teaser/
        v1/
            teaser/
        v2/
            teaser/

Notice the version is a folder, and the folder is part of the resource type: core/wcm/components/accordion/v1/accordion versus core/wcm/components/accordion/v2/accordion are two entirely different resource types that happen to render conceptually the same thing.

Why version this way instead of just updating v1 in place?

Because Core Components make no promise that v2 is backward compatible with v1's markup, dialog fields, or Sling Model interface. A v2 might change the HTML structure to fix an accessibility issue that required restructuring the DOM, rename a property, or split one dialog into two. If Adobe simply overwrote v1 with those changes, every site using v1 would break silently on the next Core Components upgrade — including sites that never asked to change anything.

Versioned folders solve that by making the breaking change opt-in. v1 keeps existing, unchanged, forever. v2 exists alongside it. Nothing forces you onto v2 until you deliberately point your proxy at it.

This is precisely the same reasoning file 02 used to justify the proxy pattern in the first place — decoupling your content from Adobe's paths — applied one level up. The proxy protects you from Adobe renaming or restructuring a component's internals; the version folder protects you from Adobe changing a component's contract.

1.5 A project description to adapt

"On our site we're currently running Core Components 2.x as a dependency, and most of our proxies point at the latest version available at the time we set them up — mostly v2 for things like Teaser and Image, v1 for a couple of components that haven't needed a v2 yet, like Title. We track the Core Components release notes when we do a quarterly dependency review, and we've done one real upgrade — Accordion v1 to v2 — which is one of the stories I can walk through in detail."

That answer signals you understand versioning is per-component, not a single number for "Core Components" as a whole, and that upgrading is a deliberate, tracked activity rather than something that happens automatically.


2. Core Concepts

2.1 The proxy pattern, reinforced — go read file 02 section 2.3 first

File 02 already covered the mechanics of the proxy component in full: the problem it solves (no customisation point, no version control, coupling to Adobe's paths), the file itself (a .content.xml with nothing but sling:resourceSuperType), and how the resource-type chain resolves when Sling looks for a script, dialog, or edit config.

This file does not repeat that. What it adds is what happens inside the proxy once you actually need to upgrade the version it points at — because that is the part file 02 only sketched, and it is the part interviewers push into once they have confirmed you know what a proxy is.

2.2 What actually changes between Core Component versions

Before you can talk credibly about an upgrade, you need to know what kinds of things typically change between a v1 and a v2 of a Core Component, because "what breaks" always traces back to one of these:

The Sling Model's interface. Core Components ship a Java interface for each component (for example Accordion, Teaser) plus a non-exported .impl implementation — the same interface-plus-.impl pattern file 05 covers for your own models. A v2 model can add methods, but it can also change what an existing method returns, or remove one that v1 exposed. If your project's own code calls into a Core Component's model directly — which you generally should avoid, but people do it — that is where an upgrade breaks compilation.

The HTL markup and its data attributes. The most common real-world break. Core Components attach data-cmp-* attributes and specific class names that their own client-side JavaScript reads to wire up behaviour — an accordion's expand/collapse JS looks for a specific data-cmp-hook-accordion="item" attribute, for instance. If you overrode the HTL for a component (selective override, from file 02) and a version bump changes what attribute the new JavaScript expects, your overridden markup silently stops being interactive, because your markup still matches the old contract and the new JS is looking for the new one.

The dialog structure. A v2 might split one dialog field into two, rename a property, or move a field into a different tab. If you added a field via the Sling Resource Merger (file 02, section 3.2) at a specific relative path expecting v1's dialog shape, and v2 reorganises that shape, your merged field can silently stop appearing, or appear in the wrong place.

The resource type of nested/child resources. Composite components like Teaser or Carousel sometimes change what resource type their internal child items use. If you wrote custom logic that inspects a child resource's type directly, that is another break point.

What does NOT usually change: the dialog's exposed properties for basic usage. Adobe is generally careful to keep the authored content — the actual property names an author's data is stored under — stable across versions where possible, because that is content migration, which is a much bigger deal than a markup or JS change. This is worth saying explicitly in an interview, because it is the reassuring half of the answer: most version bumps do not require a content migration, they require checking your overrides.

2.3 The upgrade mechanics, step by step

Given the above, here is the actual sequence for moving a proxy from v1 to v2 — the answer you give when asked "walk me through upgrading Accordion v1 to v2":

Step 1 — bump the dependency. Core Components version is a Maven dependency (core.wcm.components.core, or the equivalent artefact for your setup). Update the version in the project's parent POM, and confirm the new version is actually installed on the target AEM instance (Cloud Service ships a current Core Components version already; on-prem you may need to deploy the new package yourself).

Step 2 — change the proxy's sling:resourceSuperType. This is the one-line change file 02 promised:

<!-- before -->
sling:resourceSuperType="core/wcm/components/accordion/v1/accordion"

<!-- after -->
sling:resourceSuperType="core/wcm/components/accordion/v2/accordion"

Nothing about your content changes. Every existing accordion on every page still points at energy/components/accordion — your resource type never moves. Only what that resource type inherits from moves.

Step 3 — audit every override you made against v1. This is the actual work, and it is exactly the list from section 2.2: did you override the HTL? Compare it against v2's reference HTL and check the data-cmp-* hooks and class names still line up, or rewrite your override to match v2's contract. Did you add a dialog field via the Resource Merger? Check the merge still lands where you expect against v2's (possibly reorganised) dialog. Did any Java code reference the v1 model directly?

Step 4 — check clientlibs. Core Components' own JavaScript and CSS clientlibs are versioned too, and are usually embedded or depended on by category rather than by exact version path, so bumping the dependency usually brings the right JS along automatically — but if your project vendored or copied any Core Components JS to customise it (which is itself a smell, but happens), that copy needs the same review as your HTL override.

Step 5 — test on a lower environment against real content, not a fresh page. The reason this matters specifically for Core Components upgrades: a fresh page you build to test the upgrade will use whatever the current dialog produces, but your existing 400 pages have content shaped by whatever v1's dialog produced months or years ago. The upgrade has to render correctly against that historical content, not just against new content you author during testing.

Step 6 — roll out. Because the proxy's sling:resourceSuperType is a single property on a single node, and it is deployed through your normal ui.apps pipeline, the upgrade itself ships like any code change — reviewed, tested, deployed. This is the entire point of the proxy: the upgrade is a one-line, reviewable code change instead of a content migration script touching hundreds of nodes.

The interview answer, compressed:

"Bump the Core Components dependency, then change one property on the proxy — sling:resourceSuperType from v1/accordion to v2/accordion. No content changes, because content still points at our own resource type. The actual work is auditing anything we overrode — our HTL, if we changed it, has to match v2's new data-cmp-* hooks or the JavaScript stops finding what it expects; any dialog field we merged in via the Resource Merger has to still land correctly against v2's dialog shape. Then we test against existing content, not just freshly authored pages, because the upgrade has to render historical content correctly too. And because the whole thing is a one-line proxy change plus our own override files, it ships through the normal code pipeline — reviewed and tested — rather than a content migration."

2.4 The Sling Model interface +.impl pattern — tie to file 05

Every Core Component follows the same Java structure file 05 teaches as best practice for your own models: a public interface in an exported package (for example com.adobe.cq.wcm.core.components.models.Teaser) describing the component's data as getter method signatures, and a non-exported .impl package holding the actual @Model -annotated implementation class, registered with adapters = Teaser.class.

Why does this matter to you as someone extending, not writing, these components? Two reasons.

One — it tells you what's actually stable. The interface is the contract. Adobe can rewrite the entire implementation between versions and, as long as the interface's method signatures stay the same, nothing that depends on the interface breaks. When you write HTL that calls teaser.getTitle() on the model obtained via data-sly-use, you are coding against the interface, and that is exactly the part of a Core Component that changes least often between versions — which is why section 2.2 said the exposed properties are usually the stable half of an upgrade.

Two — it's what makes ComponentExporter work at all. File 17 covers ComponentExporter and the Sling Model Exporter in full, but the connection here is direct: Core Components' models implement ComponentExporter alongside their own interface (for example adapters = { ComponentExporter.class, Teaser.class }), which is exactly why every Core Component is automatically exportable as JSON at <path>.model.json with no extra work from you — the SPA Editor and any headless consumer of a Core-Component-authored page rely on this being true for every single one of them, consistently.

2.5 The delegation pattern — extending a Core Component's Java logic, not just its markup

This is the part of Core Components that most 3-4 year candidates have never had to do, and getting it right is a genuinely strong senior-level answer.

Start with the problem. Say you've proxied the Core Component Teaser. Its HTL and its model do everything you need — except one thing: your teaser cards need a computed "estimated read time" badge derived from the linked page's word count, and that logic has to live somewhere. You have three bad options and one good one.

Bad option 1 — override the HTL and put the logic there. HTL cannot run arbitrary Java (file 08 covers why deliberately), so you'd need a data-sly-use on a new model anyway — but then you've thrown away the Core Component's own model logic (URL resolution, link target handling, accessibility attributes) and have to reimplement it, or call it awkwardly from your new model.

Bad option 2 — copy the Core Component's model class into your own package and modify it. Now you own a forked copy that Adobe's next version bump does not touch, does not fix, and will drift from silently. This is exactly the coupling-to-Adobe's-internals problem the proxy pattern exists to avoid, just moved into Java instead of markup.

Bad option 3 — extend the .impl class directly (public class MyTeaserImpl extends TeaserImpl). This looks reasonable but Adobe does not guarantee the .impl classes are designed for subclassing — they live in a non-exported package specifically because they are not meant to be depended on directly. You would be compiling against an internal implementation detail that can change or even become final in a later version with no notice.

The good option — delegate, using @Via(type = ResourceSuperType.class). Write your own Sling Model that implements the same interface the Core Component implements, and inject an instance of the Core Component's interface adapted from the resource's super type as a delegate field. Your model then implements every interface method by forwarding to the delegate, except the one method you actually need to change or add to.

package com.energy.core.models;

import com.adobe.cq.wcm.core.components.models.Teaser;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.via.ResourceSuperType;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;

import javax.annotation.PostConstruct;

@Model(
        adaptables = Resource.class,
        adapters = Teaser.class,
        resourceType = "energy/components/teaser"
)
public class EnergyTeaserModel implements Teaser {

    // The Core Component's OWN model, adapted from the super type this
    // resource inherits from via sling:resourceSuperType. This is the
    // delegation: we are not extending Adobe's implementation class, we
    // are asking Sling to build one for us and forwarding to it.
    @Via(type = ResourceSuperType.class)
    @SlingObject
    private Teaser delegate;

    private String readTime;

    @PostConstruct
    protected void init() {
        // Our one piece of added behaviour.
        this.readTime = computeReadTime(delegate.getLinkURL());
    }

    // Every existing Teaser behaviour is untouched -- we simply forward.
    // If Adobe fixes a bug in getLinkURL() in the next version, we get
    // that fix for free because we never reimplemented it.
    @Override public String getTitle()          { return delegate.getTitle(); }
    @Override public String getDescription()    { return delegate.getDescription(); }
    @Override public String getLinkURL()        { return delegate.getLinkURL(); }
    @Override public String getImageSource()    { return delegate.getImageSource(); }
    // ... remaining Teaser interface methods, all forwarded ...

    // The one method that's actually new -- not part of the Teaser
    // interface, so add it as a plain getter and call it from HTL directly.
    public String getReadTime() {
        return readTime;
    }

    private String computeReadTime(String linkUrl) {
        // ... word-count lookup against the linked page, omitted ...
        return "4 min read";
    }
}

Why this is the strong answer. You get the Core Component's existing behaviour for free — including whatever Adobe fixes in it later — because you never reimplemented it, you asked Sling to hand you an instance built exactly the way Adobe intended (adapted via the resource's own super type) and forwarded to it. You add exactly the one thing you need. And you have not coupled yourself to any internal Adobe class, only to the public interface, which is the stable, documented contract.

The interview answer:

"If I need to extend a Core Component's actual logic, not just its markup, I don't subclass Adobe's .impl class — that's an internal implementation detail Adobe doesn't guarantee is stable. Instead I write my own model implementing the same public interface, inject the Core Component's own model as a delegate using @Via(type = ResourceSuperType.class) — which adapts the resource through whatever sling:resourceSuperType it inherits from — and forward every interface method to that delegate except the one I'm adding or changing. That way I get all of Adobe's existing behaviour, including any future bug fixes, for free, and I've only coupled myself to the public interface, which is the part of a Core Component that changes least between versions."

Cross: Why not extend the .impl class directly? · What does ResourceSuperType mean as a @Via type versus the default? · What happens to this pattern across a version upgrade? (Usually nothing, if the interface didn't change — which is exactly why interfaces are the stable half.)

2.6 Extending a Core Component — the four ways, ranked

Pulling file 02's decision tree and the above together, here is the complete list of ways to extend a Core Component, from lightest to heaviest:

1. Style System. No code, no new resource type. Covered in full in section 2.7 onward. Use this when the requirement is purely visual.

2. Add a dialog field via the Sling Resource Merger. File 02, section 3.2. A partial dialog XML with sling:orderBefore, merged with the inherited one. Use this when you need one or two extra author-facing fields and the existing markup and logic are otherwise fine.

3. Override the HTL only. Your own .html file in the proxy folder, same file name as the folder, everything else — dialog, edit config, Java model — still inherited. Use this when the Core Component's data and behaviour are right but the design system needs fundamentally different markup.

4. Delegate to the Core Component's model. Section 2.5. Use this when you need new computed data or genuinely different logic, not just different markup, and you still want the underlying component's existing behaviour.

Notice the ordering is the same shape as file 02's decision tree — reach for the lightest mechanism that solves the actual requirement, and only go further down the list when the lighter ones genuinely cannot do it.

2.7 The Style System — the problem it solves, and where it actually lives

Start with the problem, because "Style System" as a name tells you nothing.

The problem. An author needs a component to look different in different places — a Teaser that's usually a plain card but needs a "featured, larger, bordered" treatment on the homepage, or a Text component that needs a "pull quote" look on an article page. The naive fixes are both bad. Building a second component just for the visual difference means two dialogs, two HTL files and two things to keep in sync for content that is otherwise identical. Adding a "variation" dropdown to the existing dialog means a developer has to add code every time marketing wants a new visual option, and — the part people miss — that dropdown's options are the same everywhere the component is used, because they're hardcoded in one dialog XML file shared by every instance.

The Style System is Adobe's answer to both problems, and the mechanism is worth stating precisely:

Style classes are defined in the component's policy, not in its dialog, and not in its code.

That single sentence is the whole idea, and it is why the Style System ties directly back to file 03. A policy, remember, is design-time configuration for a component as used within a particular template. Up to now every policy example in file 03 was about permissions — which components are allowed inside a container. The Style System is what a policy configures when the component in question is not a container but an ordinary content component: instead of "what can go inside you," the policy says "what can you look like, here, on this template."

The actual property, cq:styleGroups. Inside a component's policy definition, a cq:styleGroups node holds one or more style groups, and each group holds one or more individual styles:

/conf/energy/settings/wcm/policies/energy/components/teaser/product_teaser_policy/
    cq:styleGroups/
        background/                    ← one style GROUP
            cq:styleGroupLabel = "Background"
            cq:styles/
                light/
                    cq:styleId = "2001"
                    cq:styleLabel = "Light"
                    cq:styleClasses = "teaser--bg-light"
                dark/
                    cq:styleId = "2002"
                    cq:styleLabel = "Dark"
                    cq:styleClasses = "teaser--bg-dark"

Four properties worth being able to name individually, because interviewers ask for them by name:

cq:styleGroupLabel — the heading an author sees in the Styles panel, grouping related choices together.

cq:styleId — a stable numeric-looking ID. This, not the class name, is what gets written onto the content node when an author picks a style. That indirection is deliberate — content records which choice was made, and the policy is the only place that records what that choice currently means.

cq:styleLabel — the human-readable name for this specific style, inside its group, shown to the author.

cq:styleClasses — one or more CSS class names, space-separated if more than one, applied to the component's markup when this style is selected.

How the class actually reaches the markup. File 02, section 3.3 already introduced the decoration wrapper — the <div> AEM adds around every component so the page editor has something to attach its toolbar to. The Style System's chosen classes are added to exactly that wrapper, not to some element inside your HTL. That is worth stating as its own fact, because it explains a real class of confusion: if your HTL's own root element is not the decoration wrapper — for instance because you changed the decoration tag name, or disabled decoration entirely with cq:noDecoration — the style class still needs somewhere to land, and disabling decoration without a plan for where style classes go is a real way to make the Style System silently stop working for that component.

2.8 Single-select versus multi-select style groups

By default, a style group behaves like a radio choice: an author picks exactly one style from it, or none. The "Background" group above is exactly this — light or dark, never both, because a background genuinely can only be one thing at a time.

A style group is single-select by default: the author picks one style from it, like a radio button. Ticking "Styles can be combined" on the group in the policy dialog (stored as cq:styleGroupMultiple) makes it multi-select, so the author can apply several styles from that one group at once. Separately, different style groups are always independent of each other. If the policy also defines an "Emphasis" group with a single "Featured" style in it, an author can pick a background and independently decide whether to also apply "Featured" — the two choices don't interact, and both sets of resulting classes land on the same wrapper together.

Say it like this in an interview, because it corrects a common misconception in one sentence:

"Each style group is single-select by default — the author picks one style, like a radio button. Ticking 'Styles can be combined' on a group, which sets cq:styleGroupMultiple, lets the author pick several styles from that group. And choices from different groups are always independent and combine on the same wrapper."

The practical design consequence: if two style groups' classes might visually conflict when both are chosen — say a "Dark" background and a border colour meant only for light backgrounds — that has to be handled in the CSS itself, because the Style System has no concept of one group depending on another's choice.

2.9 Why the Style System beats a dialog "variation" dropdown — and the multi-brand argument

This is one of the highest-value comparisons in this file because it comes up constantly, usually as "why not just add a dropdown?" Give all four reasons, not just one:

No code, no deployment. Adding a new style is a policy edit through the template editor's Styles configuration UI. Adding a new dropdown option is a dialog XML change, which is code, which needs a deployment.

Centrally controlled per template. A dialog's options live in one XML file shared by every instance of the component everywhere it's used. A policy is per-template (file 03's whole point), so the exact same proxied component can offer a completely different set of styles on one template than on another.

Owned by the right role. A template author can manage styles through the UI. A dropdown's options are owned by a developer, because they're in code.

It scales to multi-brand. This is the argument worth developing fully, because it is where the Style System's design pays for itself on a real project. Tie it to file 12's territory: our twenty-country energy site runs each country as an MSM live copy of a language master, and different countries can resolve different /conf configuration through context-aware configuration and sling:configRef (file 03, section 3.4). Because a policy lives under /conf, two countries pointing at different configuration roots can define entirely different style groups for the exact same proxied Core Component, with zero code differences:

/conf/energy-eu/settings/wcm/policies/.../teaser_policy
    cq:styleGroups → Light / Dark

/conf/energy-apac/settings/wcm/policies/.../teaser_policy
    cq:styleGroups → Light / Dark / High-Contrast   (a regional accessibility requirement)

Same Teaser component, same Java, same HTL, same proxy. One region simply has a policy with one more style group in it. A dialog dropdown cannot do this at all — its options are baked into one file the whole codebase shares.

The interview answer, in full:

"A dropdown's options are hardcoded in the dialog, so every instance everywhere gets the same choices, and adding one means a developer, code, and a deployment. A Style System style is defined in the component's policy instead, so a template author manages it through the UI with no deployment, and because policies are per-template — and per /conf configuration root — the same component can offer completely different style options on different templates, or even different countries on a multi-brand site, without touching a line of code."

Cross: Where exactly does the class land? (The decoration wrapper.) · What happens if a style is removed from the policy after being chosen? (Silently stops applying — the stored ID no longer resolves to anything; see the Common Mistakes and Scenarios tables.) · How does this tie to MSM? (Different live copies can resolve different policies via sling:configRef.)

2.10 Data layer support

The problem this solves. Analytics and personalisation tooling needs to know what is on a page and what happened on it — which teaser was clicked, which accordion panel was opened — without scraping the DOM for CSS class names or text content, because those change with every redesign and are never a stable contract for a downstream analytics tag to depend on.

Core Components ship built-in support for the Adobe Client Data Layer pattern to solve exactly this. Two halves to know:

Description. A component eligible for the data layer renders a data-cmp-data-layer attribute on its markup, containing a JSON description of itself — its own generated ID, its component type, and a handful of component-specific fields (a Teaser's title, or the DAM path of its image, for instance). That JSON is present in the page's own HTML, so any script reading the data layer does not need to infer anything from visual markup — it reads a structured, versioned description that Adobe defines the shape of.

Events. On genuine user interaction — an accordion panel expanding, for instance — the component's own JavaScript pushes an event object onto a well-known global array (conventionally window.adobeDataLayer), which analytics tooling listens to. That decouples "something happened" from "how the DOM happened to be structured at that moment," which is exactly the fragility a scraping-based analytics integration suffers from.

Where this is configured. Data layer support is generally controlled by a project-wide toggle (an OSGi configuration enabling it globally) with the ability to enable or disable it per component through its policy — the exact property naming can differ by Core Components version, so rather than quoting one, know the shape: a global switch, and a per-component override, both design-time, neither requiring you to write the JSON by hand.

Why this matters to you as someone extending, not writing, Core Components: if you override a component's HTL, you can accidentally drop the data-cmp-data-layer attribute along with everything else you didn't reproduce, silently breaking analytics tracking for that component with no visible symptom on the page itself — nothing looks wrong, the numbers are just quietly missing. That is worth naming as a checklist item alongside file 02's accessibility caution, because both are invisible failure modes an override can introduce without anyone noticing until someone asks why a report has a gap in it.

2.11 Accessibility — what "built to WCAG standards" actually means in practice

File 02 already gave you the one-line argument: Core Components are built to WCAG standards, and extending one inherits that work for free. Here is what that claim actually cashes out to, because "accessible" said without specifics sounds like a slogan, and with specifics sounds like you've read the markup.

Semantic elements over div soup. A <button> for anything clickable and interactive, not a <div> with a click handler — the file 02 FAQ accordion example showed exactly why: a real button is keyboard-focusable and activatable with Enter or Space for free, with no JavaScript required to make it so.

The full ARIA contract where semantics alone aren't enough. Interactive composite components — Accordion, Tabs, Carousel — follow the WAI-ARIA authoring pattern for that widget type: aria-expanded on a toggle, aria-selected on a tab, aria-controls linking a control to what it affects, role="region" or role="tabpanel" marking the controlled content as a landmark. File 02, section 8B.4 walked through this table for the accordion in full, and the same shape of contract applies to Tabs and Carousel.

Keyboard operability, not just focusability. Being reachable by Tab is necessary but not sufficient — Tabs and Carousel additionally support the arrow-key navigation pattern users of assistive technology expect for that widget type, not just Tab-and-Enter.

Alt text as an authoring requirement, not an afterthought. The Image component's dialog includes a field for alternative text, and — importantly — a way to mark an image as purely decorative, which should render an empty alt="" rather than either omitting the attribute (which some screen readers announce as the filename) or forcing an author to invent a description for an image that conveys no content.

Focus management on things that show and hide content. A component that opens a panel or a modal-like overlay needs to consider where keyboard focus goes when it opens and where it returns when it closes — this is one of the genuinely hard parts of accessible interactive UI, and it is exactly the kind of thing a from-scratch custom component is most likely to get wrong under a deadline.

Adobe tests for this as part of Core Components' own release process — automated accessibility tooling as part of their CI, alongside manual review. That is the concrete backing for "tested," not just "written carefully."

The consequence for you. Every one of these is work you do not have to redo when you extend rather than rebuild — but every one of these is also something you can silently throw away the moment you override a Core Component's HTL and don't reproduce it. "We override the HTL for design reasons" is a completely normal, defensible answer. "We override the HTL and never checked whether the ARIA attributes survived" is the answer that turns an accessibility audit into a bad afternoon. Worth stating explicitly:

"Extending Core Components is a real accessibility argument, but it's only as good as what you preserve when you customise. If I override HTL, checking the ARIA contract and keyboard behaviour survived the change is part of the review, the same way checking the data-cmp-* hooks survived is part of a version upgrade."

2.12 The Responsive Grid — the Layout Container in depth

File 03 already introduced the Layout Container as the component behind wcm/foundation/components/responsivegrid, and showed it sitting inside a template's structure as the authoring surface authors drop components into. This section goes deeper into how its responsiveness actually works, because "it's responsive" is not an answer an interviewer accepts on its own.

The problem it solves. Before a mechanism like this, making a page look right at three or four different viewport widths meant a developer writing bespoke CSS media queries for every possible arrangement of components an author might create — which does not scale, because authors rearrange pages constantly and a developer cannot pre-write a media query for every future arrangement.

The authoring model — column-based layout. The Layout Container divides its width into a fixed number of columns (conventionally twelve, matching the common web-grid convention), and each direct child is assigned how many columns wide it should be, independently per breakpoint. A hero image might span all twelve columns on mobile but sit at six columns beside a six-column text block on desktop — same two components, same content, different arrangement, purely a layout decision.

Where this is set — Layout mode. In the template editor (file 03) or, for unlocked containers, directly in the page editor, a template author switches into Layout mode, picks a breakpoint from a device-icon toolbar (typically something like Desktop, Tablet, and Mobile, plus any custom breakpoints the project has defined), and drags column boundaries to resize each child, or drags components to reorder them, independently for each breakpoint selected. This is a genuinely different mode from ordinary authoring — you are configuring "how do things arrange at this width," not editing any component's content.

Breakpoints themselves are configurable, not fixed. Beyond the defaults, a project can define its own breakpoints — named viewport-width thresholds under a project's /conf configuration — so a project whose design system cares about an unusual width (say, a specific tablet-landscape breakpoint the site's design mandates) is not stuck with only the out-of-the-box set.

How the grid CSS is actually generated — the mechanism to state precisely. The column span an author sets for each child, per breakpoint, is stored on the container's own content node and resolved into CSS class names on each child's decoration wrapper — something in the shape of aem-GridColumn--width-<breakpointName>--<columnSpan>, with a similar pattern for any column offset. Those classes are static, generated once at author time. The Layout Container's own clientlib CSS then does nothing more than define, inside a @media rule matching each breakpoint's width, what width (or grid/flex sizing) each of those classes maps to. Nothing computes at render time on the server, and nothing needs JavaScript in the browser either — the browser's own CSS engine evaluates which media query currently applies and renders accordingly.

Why that mechanism matters, stated as the payoff. Because the markup is static per page — the same HTML is served to every visitor regardless of their actual viewport — the page is exactly as dispatcher-cacheable as a page with no responsive behaviour at all. This is the detail that turns "it's responsive" into a real architectural answer: responsiveness here is a CSS-only concern layered on top of markup that never changes per request.

The emulator — how authors preview breakpoints. The template and page editors include a device-emulator toolbar in Layout and Preview modes, letting an author see the page rendered at a given breakpoint's width without needing an actual device of that size — essential for a template author configuring column spans to see immediately what each breakpoint will actually look like rather than authoring one breakpoint's layout blind to the others.

Its relationship to the site's own CSS grid or flexbox — and when NOT to use it. The Layout Container generates and owns its own grid CSS. A component that already implements its own internal layout using CSS Grid or Flexbox — say, a card component whose internal layout is entirely its own concern — should not have a Layout Container nested inside it "just in case," because now two grid systems are trying to size the same elements, and they were not designed to cooperate. The Layout Container is the right tool for page-level, author-controlled arrangement of independent components. It is the wrong tool inside a single component's own internal markup, and it is also the wrong tool for a design that needs to be pixel-consistent and centrally controlled with no per-page authoring freedom at all — some projects deliberately give authors a fixed arrangement with no Layout mode access on certain templates, precisely because "the marketing page must always look exactly like this" is a real, valid requirement that a fully free-form authoring grid works against rather than for.

The interview answer:

"The Layout Container is the component behind the responsive grid — authors set how many of twelve columns each child spans, independently per breakpoint, in the template editor's Layout mode. That gets baked into CSS class names on each child at author time, and the Layout Container's own CSS applies width rules to those classes inside media queries per breakpoint — nothing is computed live, so the markup is exactly as cacheable as a non-responsive page. I'd use it for page-level arrangement authors genuinely need to control. I would not nest it inside a component that already owns its own internal CSS grid or flexbox layout, because you end up with two grid systems fighting over the same elements — and on a design that must render identically everywhere with no per-page authoring freedom, I'd consider not exposing Layout mode at all rather than giving authors a tool that invites drift from the design system."

Cross: What generates the column-width CSS classes? · Why doesn't a structure change to the grid need a dispatcher-cache-busting mechanism beyond a normal flush? · How would you add a custom breakpoint? · What happens if you nest a Layout Container inside a component with its own flexbox layout?

2.13 When NOT to use Core Components

Said honestly, because an interviewer respects a bounded answer more than blanket enthusiasm:

When the requirement has no conceptual equivalent. File 02's example is the exact right one — a listing whose cards are derived from the page tree rather than hand-authored has no Core Component analogue. Building it "Core-Component-style" (Sling Model, HTL-only rendering, accessible markup) is still good practice, but there is no v1/v2 to inherit from.

When the design system's markup requirements are so different that overriding the HTL becomes a full rewrite anyway. At that point you are paying for the inheritance chain, the dialog you don't use as-is, and the version-tracking overhead, without getting much benefit. This is rare, but it happens with heavily custom design systems — worth naming as an exception rather than pretending it never applies.

When a component genuinely needs server-side integration logic with no authoring surface at all — for example a component that renders purely from a backend service call with no dialog fields — a Core Component base gives you very little, since most of what it offers is dialog and authoring behaviour.

The point of naming these is not to talk yourself out of Core Components — the default is still "yes, proxy one" — but to show you have actually hit a case where the default did not apply, which is more convincing than reciting "always use Core Components."


3. Internal Working

3.1 The version-folder resolution, end to end

graph TD A["Content node<br/>sling:resourceType = energy/components/accordion"] --> B["/apps/energy/components/accordion<br/>the PROXY"] B -->|"sling:resourceSuperType"| C["/apps/core/wcm/components/accordion/v2/accordion<br/>Adobe's v2, if proxy points here"] C -.->|"v2's own sling:resourceSuperType,<br/>if it declares one for shared base logic"| D["shared accordion base, if any"] B --> E{"Does the proxy<br/>override accordion.html?"} E -->|Yes| F["Proxy's own HTL used"] E -->|No| G["v2's HTL used, via resourceSuperType fallback"]

The point to draw out verbally: the content node's resource type never changes across a version upgrade. Only the single property on the proxy — sling:resourceSuperType — changes which version folder that resource type's missing pieces fall through to. That is why the upgrade is a one-line change rather than a content migration, and it is worth saying explicitly because it is the same resolution mechanism file 02 already taught for the basic proxy, just pointed at a different version folder.

3.2 How the Style System applies a class at render time

sequenceDiagram participant Au as Author participant Ed as Page Editor participant Pol as Component Policy<br/>(cq:styleGroups) participant Node as Component's content node participant HTL as Component HTL / decoration Au->>Ed: selects component, opens "Styles" Ed->>Pol: read cq:styleGroups for this component's policy Pol-->>Ed: list of style groups and their cq:styleClasses Au->>Ed: picks a style (e.g. "Dark") Ed->>Node: write cq:styleIds property on the component node Node-->>HTL: on render, resolve cq:styleIds against the policy again HTL->>HTL: add the resulting cq:styleClasses to the DECORATION wrapper

Why this matters as a mechanism, not just a fact: the author's choice is stored as an ID (cq:styleIds, pointing at cq:styleId in the policy), not as the literal class name. That indirection is exactly what makes the Style System centrally controlled — the class a given style ID maps to lives in the policy, so a template author (or developer) can change what "Dark" actually renders as, for every component that used it, by editing the policy once, without touching a single piece of content. Content stores the choice; the policy stores the meaning of that choice.

3.3 The Responsive Grid's render-time column resolution

graph TD A["Layout mode in the template editor"] --> B["Author sets column widths<br/>per breakpoint per child"] B --> C["Stored as cq:responsive on the<br/>Layout Container's content node"] C --> D["Layout Container HTL emits<br/>CSS classes encoding width per breakpoint<br/>e.g. aem-GridColumn--width-tablet--6"] D --> E["Core Components' grid CSS<br/>(clientlib) maps those classes<br/>to actual column widths via media queries"] E --> F["Browser renders the grid<br/>at whatever breakpoint is active"]

The mechanism, stated plainly: the Layout Container does not compute layout live in the browser with JavaScript. It computes the column widths once, at author time, in Layout mode, and bakes them into class names on each child's decoration wrapper. The CSS shipped with the Layout Container's clientlib then does nothing more than apply width (or grid/flex sizing) rules keyed to those class names inside media queries matching each breakpoint. That is why the Responsive Grid is dispatcher-cache-friendly — the markup is static per page, and the browser's own media query evaluation handles the "which layout applies right now" question with no server involvement at all.


8. Coding / Configuration Examples

8.1 The proxy, before and after an upgrade

/apps/energy/components/accordion/.content.xml — before:

<?xml version="1.0" encoding="UTF-8"?>
<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="Accordion"
    componentGroup="Energy - Content"
    sling:resourceSuperType="core/wcm/components/accordion/v1/accordion"/>

After the upgrade — the entire change:

<?xml version="1.0" encoding="UTF-8"?>
<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="Accordion"
    componentGroup="Energy - Content"
    sling:resourceSuperType="core/wcm/components/accordion/v2/accordion"/>

Nothing else changes. No content node anywhere in the repository is touched by this file. Every existing accordion still has sling:resourceType="energy/components/accordion" and picks up v2's behaviour the moment this deploys.

8.2 A dialog field added via the Resource Merger, re-verified after the upgrade

/apps/energy/components/accordion/_cq_dialog/.content.xml (partial — merges with v2's inherited dialog)

<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:sling="http://sling.apache.org/jcr/sling/1.0"
          xmlns:jcr="http://www.jcp.org/jcr/1.0"
    jcr:primaryType="nt:unstructured"
    sling:resourceType="cq/gui/components/authoring/dialog">
    <content jcr:primaryType="nt:unstructured">
        <items jcr:primaryType="nt:unstructured">
            <columns jcr:primaryType="nt:unstructured">
                <items jcr:primaryType="nt:unstructured">
                    <column jcr:primaryType="nt:unstructured">
                        <items jcr:primaryType="nt:unstructured">

                            <!-- OUR addition: not part of Core Components' own
                                 accordion dialog at all. We rely on the Resource
                                 Merger to fold this into whatever v2's dialog
                                 shape actually is at this relative path. -->
                            <analyticsId
                                jcr:primaryType="nt:unstructured"
                                sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
                                fieldLabel="Analytics Component ID"
                                fieldDescription="Used to tag accordion interactions in analytics tooling"
                                name="./analyticsId"
                                sling:orderBefore="*"/>

                        </items>
                    </column>
                </items>
            </columns>
        </items>
    </content>
</jcr:root>

Why this needs re-checking on every version bump, said plainly in a comment developers will actually read: if v2 restructures its own columns/column/items nesting — for example splitting the dialog into tabs — this partial dialog's relative path assumption (content/items/columns/items/column/items) may no longer land where you expect, and the field can silently stop appearing or appear in the wrong tab. This is exactly the audit step from section 2.3.

8.3 The delegation pattern, complete, with the OSGi/Sling Model wiring made explicit

core/src/main/java/com/energy/core/models/EnergyTeaserModel.java

package com.energy.core.models;

import com.adobe.cq.wcm.core.components.models.Teaser;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.models.annotations.DefaultInjectionStrategy;
import org.apache.sling.models.annotations.Model;
import org.apache.sling.models.annotations.Via;
import org.apache.sling.models.annotations.via.ResourceSuperType;
import org.apache.sling.models.annotations.injectorspecific.SlingObject;
import org.apache.sling.models.annotations.injectorspecific.ValueMapValue;

import javax.annotation.PostConstruct;
import java.util.List;

/**
 * Extends the Core Component Teaser's LOGIC, not just its markup.
 *
 * We do NOT subclass Adobe's TeaserImpl -- that class lives in a
 * non-exported .impl package specifically because Adobe does not promise
 * it is safe to extend. Instead we implement the same public interface
 * and delegate to an instance Sling builds for us from whatever
 * sling:resourceSuperType this resource actually inherits from right now.
 *
 * The practical payoff: if the proxy's resourceSuperType is later bumped
 * from v1 to v2, this class does not need to change at all, as long as
 * the Teaser interface itself is unchanged -- we automatically delegate
 * to whichever version is now wired in.
 */
@Model(
        adaptables = Resource.class,
        adapters = Teaser.class,
        resourceType = "energy/components/teaser",
        defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL
)
public class EnergyTeaserModel implements Teaser {

    @Via(type = ResourceSuperType.class)
    @SlingObject
    private Teaser delegate;

    // Our own extra dialog field, added via the Resource Merger.
    @ValueMapValue
    private String badgeText;

    private String readTimeBadge;

    @PostConstruct
    protected void init() {
        // Only the NEW behaviour lives here. Everything else forwards.
        this.readTimeBadge = StringUtils.isNotBlank(badgeText)
                ? badgeText
                : computeReadTimeFromLink(delegate.getLinkURL());
    }

    // --- Every existing Teaser method: pure forwarding ---

    @Override
    public String getTitle() {
        return delegate.getTitle();
    }

    @Override
    public String getDescription() {
        return delegate.getDescription();
    }

    @Override
    public String getLinkURL() {
        return delegate.getLinkURL();
    }

    @Override
    public String getImageSource() {
        return delegate.getImageSource();
    }

    @Override
    public List<?> getActions() {
        // Forwarding a method whose return type we don't touch at all --
        // this is the common case: most of an interface just passes through.
        return delegate.getActions();
    }

    @Override
    public boolean isImageLinkHidden() {
        return delegate.isImageLinkHidden();
    }

    // --- Our one addition: not part of the Teaser interface ---

    public String getReadTimeBadge() {
        return readTimeBadge;
    }

    private String computeReadTimeFromLink(String linkUrl) {
        // ... word-count lookup against the linked page's content, omitted ...
        return "4 min read";
    }
}

HTL calling this model — unchanged from how you'd call the plain Core Component's model:

<sly data-sly-use.teaser="com.energy.core.models.EnergyTeaserModel"/>

<div class="cmp-teaser">
    <a href="${teaser.linkURL @ context='uri'}" data-sly-test="${!teaser.imageLinkHidden}">
        <img src="${teaser.imageSource}" alt=""/>
    </a>
    <h3 class="cmp-teaser__title">${teaser.title}</h3>
    <p class="cmp-teaser__description">${teaser.description}</p>

    <!-- The one thing our delegation added -->
    <span class="cmp-teaser__badge" data-sly-test="${teaser.readTimeBadge}">${teaser.readTimeBadge}</span>
</div>

8.4 The Style System policy — single-select and multi-select groups together

/conf/energy/settings/wcm/policies/energy/components/teaser/.content.xml (extract)

<product_teaser_policy
    jcr:primaryType="nt:unstructured"
    jcr:title="Product Teaser"
    sling:resourceType="energy/components/teaser">

    <cq:styleGroups jcr:primaryType="nt:unstructured">

        <!-- Group 1: SINGLE-SELECT (background). Authors pick exactly one. -->
        <background
            jcr:primaryType="nt:unstructured"
            cq:styleGroupLabel="Background">
            <cq:styles jcr:primaryType="nt:unstructured">
                <light
                    jcr:primaryType="nt:unstructured"
                    cq:styleId="2001"
                    cq:styleLabel="Light"
                    cq:styleClasses="teaser--bg-light"/>
                <dark
                    jcr:primaryType="nt:unstructured"
                    cq:styleId="2002"
                    cq:styleLabel="Dark"
                    cq:styleClasses="teaser--bg-dark"/>
            </cq:styles>
        </background>

        <!-- Group 2: a SEPARATE style group, so it's chosen independently of
             the background group above. Choices from separate groups always
             combine on the same wrapper. (Within one group, set
             cq:styleGroupMultiple="{Boolean}true" to allow several picks.) -->
        <emphasis
            jcr:primaryType="nt:unstructured"
            cq:styleGroupLabel="Emphasis">
            <cq:styles jcr:primaryType="nt:unstructured">
                <featured
                    jcr:primaryType="nt:unstructured"
                    cq:styleId="2003"
                    cq:styleLabel="Featured (larger, bordered)"
                    cq:styleClasses="teaser--featured"/>
            </cq:styles>
        </emphasis>

    </cq:styleGroups>
</product_teaser_policy>

What an author sees: two independent choices in the Styles panel — a background pick (light or dark, one or the other) and an optional "Featured" toggle. Choosing dark background plus featured results in both teaser--bg-dark and teaser--featured landing on the decoration wrapper together — separate groups always combine like this. To let an author pick several styles from within one group, tick "Styles can be combined" on that group (cq:styleGroupMultiple).

8.5 A CI check that would have caught Story 3's mistake

ci/scripts/check-core-component-references.sh (conceptual — the shape of the check, not a specific CI vendor's syntax)

#!/bin/bash
# Fails the build if any content package references a Core Component's
# resource type directly instead of going through a project proxy.
#
# Why this exists: a single content node referencing
# core/wcm/components/... directly is invisible in normal review -- it
# renders identically to a proxied component until Adobe ships a version
# change, at which point it breaks with no warning, on whatever schedule
# the platform's Core Components version happens to move.

MATCHES=$(grep -rl 'sling:resourceType="core/wcm/components/' ui.content/src/main/content/jcr_root || true)

if [ -n "$MATCHES" ]; then
    echo "ERROR: content references a Core Component's resource type directly."
    echo "Route through a project proxy under /apps/energy/components/ instead."
    echo "$MATCHES"
    exit 1
fi

9. Common Mistakes

The mistakeWhat actually happensThe fix
Content references a Core Component's resource type directlySilently fine until Adobe ships a version change on the platform, then breaks with no warningAlways go through a project proxy — add a CI check like 8.5
Overriding HTL and never re-checking it after a version bumpdata-cmp-* hooks drift from what the new JS expects; component looks fine but interactivity silently breaksDiff your override against the new version's reference HTL on every upgrade
Extending a Core Component's .impl class directlyCompiles today, but you're depending on an internal, non-exported implementation detail Adobe can change without noticeUse the delegation pattern — implement the interface, delegate via @Via(type = ResourceSuperType.class)
Testing an upgrade only against freshly authored pagesOld content, shaped by the old dialog, isn't exercised — bugs only surface in productionTest against a copy of real, existing content
Adding a "variation" dropdown to the dialog for a purely visual choiceRequires a code deployment for every new variant, and can't differ per templateUse the Style System instead
One style group trying to offer several simultaneous choicesConfusing UI — a single-select group forces one pick when several were meant to combineSplit into separate style groups; combine by using more than one group
Assuming Style System classes land on an inner elementThey land on the decoration wrapper onlyWrite CSS assuming the class is on the wrapper, or read cq:styleIds yourself for a different target
Editing a shared Style System policy without checking which templates use itEvery template pointing at that policy changes its style options at onceSame governance point as file 03 — check usage, split definitions where intent differs
Using the Responsive Grid for a layout that must be dynamic per render (e.g. sibling-count-dependent)Columns are fixed at author time; render-time dynamism silently doesn't happenHandle it in CSS (flex/grid auto-fit) or compute it in the model
Forking a Core Component's model class instead of delegatingYou now own a permanent copy that drifts from Adobe's fixes silentlyDelegate via the interface, forward everything except what's new

10. Best Practices

On proxying. Always proxy, never reference a Core Component's resource type directly from content — enforce it with a CI check if the project is large enough that a "just this once" shortcut is plausible.

On overrides. Keep a written (even informal) list of exactly which files you've overridden per proxied component — HTL, dialog additions, delegated models. When a version bump happens, that list is your entire audit checklist.

On the delegation pattern. Reach for it only when you genuinely need new or different logic, not new markup — an HTL override is cheaper and has less surface area to maintain across upgrades.

On the Style System. Prefer it over dialog dropdowns for anything purely visual. Name style groups and IDs descriptively — a future developer or template author should be able to tell what "2002 / Dark" is without opening the HTL to check.

On multi-brand. Push country- or brand-specific visual differences into separate policies under separate /conf configuration branches, not into the component's code or dialog. That keeps the component itself brand-agnostic.

On the Responsive Grid. Use it for genuine page-level, author-controlled layout. Do not reach for it inside a component that already owns its own internal CSS grid or flexbox layout — nesting the Layout Container there usually fights the component's own styling rather than helping it.

On upgrades. Treat a Core Components version bump like any dependency upgrade — scheduled, reviewed, tested against real content, not something that happens incidentally alongside an unrelated platform change without its own review.


11. Debugging Tips

Tool / checkWhat it tells you
Grep content packages for sling:resourceType="core/wcm/components/Whether any content bypasses the proxy pattern entirely — the root cause of Story 3
Compare your proxy's sling:resourceSuperType against the Core Components version actually installedWhether you're really running the version you think you are
Diff your overridden HTL against the Core Component's own reference HTL for the target versionWhether data-cmp-* hooks and class names still line up after an upgrade
Browser dev tools, inspect the decoration wrapperWhether a Style System class actually landed where expected, and which one
Open the component's policy in CRXDE, check cq:styleGroupsWhether a style an author expects to see is actually defined, and under which template's policy
resourceType.model.json on a delegated model's resourceConfirms the delegate forwarded correctly — if a field is missing, check the interface method is actually overridden or forwarded
Core Components release notes for the target versionThe authoritative list of what changed — the audit checklist for section 2.3 writes itself from this
AEM Mocks unit test against both the old and new resourceSuperTypeLets you catch a delegation or override break before deploying, by adapting the same test content against both versions

12. Performance Notes

Core Components themselves are not a performance risk by default — they're built and tested by Adobe at scale, and using them is generally a performance win over unmaintained custom code, not a cost.

The delegation pattern adds a small amount of adaptation overhead — one extra adaptTo call per delegated model per request — which is negligible next to any real rendering work, but avoid unnecessary layers of delegation stacked on delegation, which does add up and complicates debugging for no benefit.

The Style System has effectively zero runtime cost. It resolves to a class name lookup against the policy at render time and nothing more — no JavaScript, no additional network request.

The Responsive Grid's column resolution happens at author time, not render time, which is exactly why it doesn't cost anything on the request path — the classes are already baked into the markup, and the browser's CSS engine does the rest via media queries with no server involvement.

A version upgrade is not a performance event in itself, but if the new version ships additional client-side JavaScript (a common pattern when accessibility fixes require more interactive behaviour), check whether that JS is loaded efficiently through your clientlib categories (file 04) rather than duplicated by an override you no longer need.


15. Comparison Tables

Ways to extend a Core Component, ranked by weight

MethodCode requiredUse when
Style SystemNonePurely visual variant
Resource Merger dialog additionDialog XML onlyOne or two extra author fields, logic unchanged
HTL overrideHTL onlyMarkup must differ, data and behaviour are fine
Delegation patternJava modelNew or different logic, want to keep existing behaviour
Full custom componentEverythingNo Core Component equivalent exists at all

Style System versus a dialog "variation" dropdown

Style SystemDialog dropdown
Defined byTemplate author, in the policyDeveloper, in the dialog XML
Requires deployment to add an optionNoYes
Can differ per templateYesNo — same options everywhere
Where the class is appliedDecoration wrapper, automaticallyWherever the developer's HTL reads the dropdown value
Stored on content asA style IDThe literal chosen value

Extending markup versus extending logic

HTL overrideDelegation pattern
What changesPresentation onlyData, computed fields, behaviour
Underlying modelUnchanged, inheritedYour own, implementing the same interface
Coupling to AdobeResource-type inheritance onlyPublic interface only (not .impl)
Survives a version bump howNeeds re-checking against new HTL contractUsually unaffected if the interface itself is stable

Static component variants versus Style System — the Story 2 lesson

Copy-pasted "variant" componentsOne component, Style System
New visual option needsA new component, new dialog, new codeA policy edit, no deployment
MaintenanceN separate components to keep in syncOne component
Risk of driftHigh — each copy evolves independentlyLow — one source of truth

Next topic

23-Resource-Resolution-and-Sling-Resource-Merger — going deeper than file 01's introduction: the full resource resolution algorithm, how /apps and /libs overlay, the Sling Resource Merger's sling:hideResource and sling:hideProperties in complete detail, and /mnt/overlay.


Topic 22 of the AEM Interview Preparation repository. Teaching style, energy-sector project domain.