Templates & Frontend
How to Organise Stylesheets in a Joomla 6 Template
Template CSS starts tidy and ends as one long file nobody wants to touch. Most of what preprocessors were adopted for — variables, nesting, splitting files — is now either native CSS or handled by the asset manager. Here is a structure…
Custom properties do the variable work
Define the design decisions once, at the top:
:root {
--ink: #0e0d0b;
--paper: #ede7dc;
--accent: #f0842b;
--measure: 68ch;
--gap: clamp(1.25rem, 2.2vw, 2.25rem);
}
The difference from a preprocessor variable matters: these exist at runtime. They can be changed per section, per component or per media query, and read from JavaScript — none of which a compiled variable can do.
.section--inverted {
--ink: #ede7dc;
--paper: #0e0d0b;
}
Every rule inside that section now picks up the swapped values, with no duplicated declarations.
Nesting is native
Nesting no longer requires a build step:
.card {
border: 1px solid var(--rule);
& .card__title {
font-size: 1.125rem;
}
&:hover .card__title {
color: var(--accent);
}
}
Keep it shallow. Deep nesting produces selectors that are hard to override and tightly coupled to markup — a problem preprocessors were famous for and which is just as available now.
Layers make overriding predictable
The recurring problem in a Joomla template is that extension CSS and template CSS fight, and the winner depends on load order and specificity. Cascade layers make the order explicit:
@layer base, components, extensions, utilities;
@layer base {
body { font-family: var(--body); }
}
@layer utilities {
.text-center { text-align: center; }
}
Anything in a later layer wins over an earlier one regardless of specificity, so a single-class utility can beat a long extension selector without !important.
Splitting files
Two ways, and the choice depends on how much you care about requests.
Register separate assets and use only what a page needs. This is the option to reach for when a stylesheet belongs to one view — a print layout, or a component used on two pages:
$wa->registerStyle('tpl.base', 'media/templates/site/mytpl/css/base.css')
->registerStyle('tpl.article', 'media/templates/site/mytpl/css/article.css', [], [], ['tpl.base']);
$wa->useStyle('tpl.base');
if ($view === 'article') {
$wa->useStyle('tpl.article');
}
The dependency in that registration matters: it guarantees base.css loads first regardless of the order you call useStyle.
Or concatenate at build time. If you already run a build for other reasons, one bundled file is fewer requests. Avoid @import in production CSS — each import is a serial request the browser cannot start until the previous file has parsed.
When a build step still earns its place
Preprocessors have not become useless, just optional. They still pay for themselves when you need loops to generate a scale, mixins with logic, or you are working inside an existing codebase that already uses them.
What is no longer a reason: variables, nesting and splitting files. If those are the whole justification, the build step is costing you a toolchain, a lockfile and an onboarding step for nothing.
Cache busting
Whatever you produce, register it with a version:
$wa->registerAndUseStyle('tpl.base', 'media/templates/site/mytpl/css/base.css', ['version' => 'auto']);
Joomla appends a hash that changes with the file. Without it, visitors keep the previous stylesheet until their cache expires, and you spend an afternoon debugging a change that shipped correctly.