Assets (CSS) & JavaScript (AMD)
- Version
- 3.x
- Updated
How to add your own CSS and JavaScript to the CBX frontend/admin without a build step. Two things make this different from a normal Joomla extension: there is no bundler/build pipeline — JS is served as AMD modules (RequireJS) directly — and CBX has a small set of auto-detected customization entry points plus a per-view/per-property asset-injection engine that also works for XHR-loaded views. This guide maps all of it.
Read com_configbox_customization_overview.md first. For attaching CSS/JS to a specific property type,
see com_configbox_custom_properties.md §8 — this guide covers the install-wide entry points and the
RequireJS configuration; that one covers per-property asset hooks. All paths are relative to the component
root docroot/components/com_configbox/. Source references are point-in-time (component 3.4.1) — verify
against the code.
1. The mental model
- No build step. There is no webpack/rollup/npm bundling for the component. JS files are AMD
modules loaded at runtime by RequireJS (
assets/main.js). A plaindefine([...], function(){})file is all you ship. - A server→client config gateway. The server serializes an
appConfigobject into adata-app-configattribute on the RequireJS script tag;main.jsreads it (no extra XHR). This is how your customization URLs/paths reach the loader (assets/main.js:14-23, assembled inhelpers/view.php:61-114). Two of the config values are the XHR endpoints:urlEndpointBase(a/…/cb-api/__CONTROLLER__/__TASK__template) and theurlXhrfallback —server.jsposts everyrequest/submitForm/injectHtml/… call to the resolved endpoint. See../platform/joomla/com_configbox_sef_urls.md§6. - The
configbox/customnamespace maps to your customization JS dir, so any module under it is loadable by id. - Auto-detected entry points. Certain customization files load automatically just by existing — no
registration. (
custom.css,custom.js,custom_questions.js.) - A view-asset engine. Each rendered view declares its stylesheets and JS init-calls; the loader injects them, de-duplicated, on first paint and on later XHR injection. Properties plug into the same engine.
2. The auto-detected entry points (drop a file, done)
The simplest customization: create one of these files and it loads everywhere, no config or registration.
| File | Loads as | Trigger |
|---|---|---|
data/customization/assets/css/custom.css | a global stylesheet, added last so it can override | file_exists() check in KenedoView::getStyleSheetUrls() (KenedoView.php:728-730); ordered last (:660-669) |
data/customization/assets/javascript/custom.js | the AMD module configbox/custom/custom, required at app start | requireCustomJs flag set by file_exists() (helpers/view.php:82); loaded in main.js:138-140 |
data/customization/assets/javascript/custom_questions.js | a second auto entry point for question/configurator JS | requireCustomQuestionJs flag (helpers/view.php:83) |
So:
/* data/customization/assets/css/custom.css — site-wide style overrides, loaded last */
#com_configbox .cb-configurator-help { background: #f5f7ff; padding: 1rem; }
// data/customization/assets/javascript/custom.js (module id: configbox/custom/custom)
define(['cbj'], function (cbj) { // cbj is CBX's namespaced jQuery
'use strict';
cbj(document).on('click', '.cb-configurator-help', function () { /* … */ });
// Use document-delegated handlers here: this module loads once at app start.
});
custom.jsis your global init module — loaded once per page. Use document-delegated event handlers (cbj(document).on(...)) so they survive XHR view injection. For per-view/per-property init that must run on each injection, use the view-asset engine (§4) or property hooks (custom-properties §8), not here.
3. Loading your own modules — the configbox/custom namespace
main.js registers (:43):
'configbox/custom': appConfig.urlCustomAssets + '/javascript', // → data/customization/assets/javascript
So any AMD module file under data/customization/assets/javascript/ is addressable as
configbox/custom/<path-without-.js>:
data/customization/assets/javascript/widgets/gallery.js ⇒ module id "configbox/custom/widgets/gallery"
// data/customization/assets/javascript/widgets/gallery.js
define(['cbj'], function (cbj) {
return {
init: function (view) { /* wire up gallery within `view` */ }
};
});
Require it from anywhere (your custom.js, a property's getJsInitCalls*, another module):
cbrequire(['configbox/custom/widgets/gallery'], function (gallery) { gallery.init(cbj(document)); });
Useful built-in module ids you can depend on: cbj (namespaced jQuery 3.7 — always depend on this, not
a global $), cbj.ui, bootstrap/cbj.bootstrap, cbj.chosen, cbj.colorbox, plus the app modules
(configbox/configurator, configbox/cart, configbox/user, …). See the full paths map in
assets/main.js:40-72.
Context: CBX runs RequireJS under the context
'CB'and exposeswindow.cbrequirefor it (main.js:29,135). Usecbrequire, not the globalrequire, to stay in CBX's context and avoid clashes with other software on the page.
4. The view-asset engine (per-view CSS + JS, XHR-safe)
This is how CBX loads assets for a specific view, including views injected by XHR after the initial page load — the mechanism your properties and custom views plug into.
Server side: each rendered .cb-content view wrapper carries data attributes — data-stylesheets,
data-init-calls-once, data-init-calls-each — built from the view's getStyleSheetUrls() +
getPropertyStyleSheetUrls() and its JS init calls. The stylesheet list is merged, de-duplicated,
minified-variant-resolved and cache-busted (KenedoView.php:648-705).
Client side (main.js:159-302): on first paint and on every cbViewInjected event,
onViewsInjected() walks each .cb-content:not(.view-init-started) and:
-
marks the view
view-init-startedimmediately, claiming it so the nextcbViewInjecteddoes not walk it again; -
injects its stylesheets if not already in
<head>(de-duped against#cb-stylesheets); -
parses each JS init call of the form
"moduleId::method",cbrequires the modules, and callsmodule.method(view)— passing the view (a jQuery collection) as the argument; -
runs
init-calls-onceat most once per page (tracked indoneModuleCalls) andinit-calls-eachevery time the view appears; -
marks the view
view-init-doneand firescbViewInitializedonce those calls have actually run.
Knowing when a view is ready
The two markers are a start line and a finish line, and only the finish line means ready.
view-init-started is set synchronously, before anything else happens to the view, while the init
calls themselves sit inside an asynchronous cbrequire callback. So a view carries
view-init-started while its modules are still loading and its handlers do not exist yet.
view-init-done is added when they do.
That gap is silent and it bites: CBX's buttons are <a> elements with no href
(<a class="… trigger-add-to-cart">), so a click landing in the gap does nothing at all — no
navigation, no console error, nothing to see. Code that waited on the start line therefore worked on
a warm cache and failed on a cold one.
Renamed from
view-processed(2026-08-16). That name read like a finish line and was used as one all over the codebase and the E2E suite; it never was one. The pairview-init-started/view-init-donecannot be misread the same way. If you have a customization keying offview-processed, it is nowview-init-started— but check first whether it actually wantedview-init-done, because that is what almost every such use meant.
view-init-done closes it. It is added, and cbViewInitialized fired on document with the view
element as its argument, only after every init call for that view has returned:
// In-page: run after ConfigBox has bound the view's handlers.
cbj(document).on('cbViewInitialized', function(event, view) {
if (view.hasClass('view-cart')) { /* the cart's handlers are live */ }
});
// From outside the page (a browser test, a screenshot script): wait for the class.
await page.locator('.kenedo-view.view-cart.view-init-done').waitFor();
A view with no init calls at all gets both immediately — there is nothing to wait for. If an init
call throws (a moduleId::method that does not exist), the stamp deliberately never lands, so a
waiter fails against that view instead of timing out somewhere unrelated later.
It means the init calls RETURNED, not that everything they started has settled. An init call that itself
cbrequires more modules is still working after it returns, andview-init-doneis already on. Whatever a handler binds synchronously is guaranteed — that covers the delegatedtrigger-…click handlers, which is what almost every caller actually needs. Anything bound deeper needs its own marker, which is exactly what.questions-init-doneis:initConfiguratorPagereturns (andview-init-donelands) two module loads before the question types have bound theirs.
Two narrower markers predate this one and remain, because they answer narrower questions:
body.cb-admin-ready (Kenedo's shared admin handlers are bound — set once per page, so it says
nothing about a view injected afterwards) and .questions-init-done on the configurator page (every
question type has bound its handlers, which happens inside the configurator's own init).
You rarely call this engine directly. You feed it via:
- Property assets —
getStyleSheetUrls()/getJsInitCallsOnce()/getJsInitCallsEach()on a property (the most common path). Fully covered incom_configbox_custom_properties.md§8. - View assets — override
getStyleSheetUrls()on a custom view to add view-scoped CSS (it flows through the same pipeline, §2'scustom.cssis appended via this method too).
…Eachmust be idempotent. A view can be injected (and thus initialized) more than once. Scope work to the passedviewand guard already-initialized nodes with a marker class — see the property guide §8 for the canonical pattern.
5. Adding RequireJS paths & shims (third-party libraries)
To load a third-party AMD/UMD library by a clean module id (or to shim a non-AMD library), define global
functions the server picks up — cbGetCustomRequirePaths() and cbGetCustomRequireShims()
(helpers/view.php:106-114). Their return values become appConfig.customPaths / appConfig.customShims,
which main.js merges into the RequireJS config (main.js:91-106):
// In a boot/settings file or a system_overrides file (loaded at boot) — see the boot-hooks guide.
function cbGetCustomRequirePaths() {
return array(
// module id => URL (no .js extension). Point at your customization assets.
'mylib' => KenedoPlatform::p()->getUrlCustomizationAssets().'/javascript/vendor/mylib.min',
);
}
function cbGetCustomRequireShims() {
return array(
// For non-AMD libs: declare exports/deps so RequireJS can load them.
'mylegacylib' => array('deps' => array('cbj'), 'exports' => 'MyLegacyLib'),
);
}
Then cbrequire(['mylib'], function (mylib) { … }) works anywhere. Define these functions where boot code
lives (the customization settings dir or system_overrides/) — see
com_configbox_system_overrides_and_boot_hooks.md. (For a property's JS you usually don't need a custom
path — configbox/custom/... already resolves your dir, §3.)
6. Minification & cache busting (what to know)
Both are admin settings (use_minified_js, use_minified_css, use_assets_cache_buster) and are handled
for you — but a few behaviors affect how you ship files:
.minfiles are build artifacts, not sources. In the CBX repos they are gitignored and generated bybuild/minify-js.sh/build/minify-css.sh(inside the component) at packaging time. A release install has them; a development checkout does not — and does not need them:- CSS: when
use_minified_cssis on, the pipeline swapsfoo.css→foo.min.cssonly if that file exists (for both system and customization assets,KenedoView.php:674-694). So shipping a.min.cssalongside your.cssis optional; if absent, your plain file is used. - JS: when
use_minified_jsis on,.minis appended to RequireJS paths — exceptconfigboxandconfigbox/custom, which are deliberately excluded (main.js:109-132). So your customization JS modules are never expected to have.minvariants — ship plain.js. (The two auto entry points and anything underconfigbox/customload unminified by design.) - The sentinel: because a missing core
.min.jswould 404, the loader checks forassets/main.min.jsfirst (helpers/view.php,getAmdLoaderJs()): if the minified build isn't present on the install, CBX serves plain sources regardless of theuse_minified_jssetting. You never have to toggle the setting on a dev site — it self-corrects. - Cache busting appends
?version=…to system + customization asset URLs (main.js:37,KenedoView.php:696-701). After changing a file, a cache-buster bump (or disabling the buster in dev) ensures clients reload it.
7. Deployment checklist
data/customization/assets/
css/
custom.css ← auto-loaded site-wide, last (override layer)
<feature>.css ← view/property-scoped CSS (referenced from getStyleSheetUrls)
javascript/
custom.js ← auto-loaded global init module (configbox/custom/custom)
custom_questions.js ← auto-loaded question/configurator init module
<path>/<module>.js ← any AMD module → id "configbox/custom/<path>/<module>"
vendor/<lib>.js ← third-party libs (register via cbGetCustomRequirePaths)
- Site-wide tweak? Drop
css/custom.cssand/orjavascript/custom.js— they auto-load, no config. - A reusable module? Put it under
javascript/…and require it asconfigbox/custom/…(§3). Depend oncbj, not a global$; usecbrequire, notrequire. - Per-view/per-property assets? Feed the view-asset engine via property hooks (custom-properties §8) or
a view's
getStyleSheetUrls()(§4). Make…Eachinit idempotent. - Third-party library? Register a path/shim via
cbGetCustomRequirePaths()/cbGetCustomRequireShims()in a boot/system-override file (§5). - Ship plain files — no
.minneeded forconfigbox/customJS (§6);.min.cssoptional. - Bust the cache after changes (or disable the buster in dev).
- Verify manually — confirm assets load on a full page load and after an XHR view injection (admin forms often inject), and that init runs once vs. each as intended. CBX has zero automated tests.
8. Conventions & gotchas
- AMD, no build. Ship
define(...)files; there is no bundler. Load via RequireJS. - Use
cbjandcbrequire.cbjis the namespaced jQuery;cbrequirekeeps you in the'CB'RequireJS context. Don't assume a global$/require. - Auto entry points load by existing —
custom.css,custom.js,custom_questions.js. No registration; just create them.custom.cssloads last (override-friendly). configbox/custom/...is your namespace for arbitrary modules →data/customization/assets/javascript/.- Don't
.minyour custom JS —configbox/configbox/customare excluded from min-path rewriting (§6); afoo.minwould 404. …EachJS is idempotent; views inject more than once. Scope to the passedview, guard with a marker class.- Scope your CSS to
#com_configbox/the view/property wrapper so it can't leak;custom.cssis global by design but still scope its selectors. - paths/shims need a boot hook —
cbGetCustomRequirePaths/cbGetCustomRequireShimsare read at boot; define them where boot code runs (§5, boot-hooks guide).
See also
com_configbox_custom_properties.md§8 — attaching CSS/JS to a specific property type (getStyleSheetUrls,getJsInitCallsOnce/Each) — the per-property side of the same engine.../technical/com_configbox_configurator_questions.md— whatcustom_questions.jsis for: the configurator question/answer loop, theconfigurator.registerQuestionType()interface, and thecb*custom events a question module hooks into (§4, §7).com_configbox_system_overrides_and_boot_hooks.md— where to definecbGetCustomRequirePaths/…Shims(boot-time functions).com_configbox_overriding_views_and_templates.md— adding view-scoped CSS via a custom view'sgetStyleSheetUrls().assets/main.js— the AMD bootstrap: appConfig gateway (:14),configbox/custompath (:43), custom paths/shims (:91-106), min handling (:109-132), the view-asset engine (:159-302).helpers/view.php—getAmdLoaderJs()(:61): theappConfigassembly, the auto-detected entry-point flags (:82-83), and thecbGetCustomRequirePaths/Shimshooks (:106-114).external/kenedo/classes/KenedoView.php— the stylesheet pipeline (getStyleSheetUrls:713, optimization/ordering:648-705).