Live This site runs Joomla 6.1.2
JoomClub

News, security and craft for the Joomla ecosystem

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.