Templates and Template Styles

This content was generated with the assistance of AI and should be verified against the official Joomla documentation before being relied on in production.

This sub-section’s Joomla bibliography lists the reference material consulted while preparing these pages.

A Joomla template is the extension that turns the output of the main component and of every module into a complete HTML page. A template style is a saved set of that template’s parameters (a logo, a colour scheme, a font choice) plus the information about where it applies. One template can have many styles, and that split is what lets a single installed template look different on the home page, in the member area and in the German section of a site. This page explains both halves for the people who work with them: site builders and administrators, who create styles and assign them to menu items, and themers, who write templateDetails.xml, index.php and the other entry files.

It was written against Joomla 6.2 (current, requiring PHP 8.3 or newer) and checked against the official documentation and the 6.2 source tree on 2026-10-30. Joomla 5.4 only receives security fixes and Joomla 4.x and 3.x are end of life; where the 5.x behaviour differs the text says so. The examples belong to the Tallpine Hiking Club, the fictitious mountain club used across this sub-section, whose site runs the child template cassiopeia_tallpine of Cassiopeia. A child template is only introduced here as far as templateDetails.xml needs it; its mechanics are in Child templates, the Cassiopeia internals are in Cassiopeia and Cassiopeia Extended, and a template written from an empty folder is in Building a template from scratch.

Site templates and administrator templates

Joomla runs two applications and each has its own template. Which one is meant is stored as a client_id (0 for the site, 1 for the administrator) and written as client="site" or client="administrator" in the manifest.

Site template Administrator template

Shipped with Joomla

Cassiopeia, and since Joomla 6.0 the child template Cassiopeia Extended

Atum

client in the manifest

site

administrator

PHP files

templates/<name>/

administrator/templates/<name>/

Public assets (copied from the media folder of the package)

media/templates/site/<name>/

media/templates/administrator/<name>/

Management screen

System → Site Templates and Site Template Styles

System → Administrator Templates and Administrator Template Styles

Entry files

index.php, component.php, error.php, offline.php

index.php, component.php, error.php, login.php, cpanel.php and more

Both kinds are ordinary extensions of type template (see Extension types, manifests and packaging): they are installed, updated and uninstalled like any other extension, and their row in the #__extensions table carries the type, element (the folder name) and client_id. The Administrator template is covered in The Administrator template Atum; the rest of this page concentrates on site templates because that is what visitors see, but template styles, templateDetails.xml and jdoc:include work identically in the Administrator.

A third, hidden template is worth knowing. The folder templates/system is not selectable in the Administrator. It holds the last-resort copies of index.php, component.php, error.php and offline.php, used when a template lacks a file and nothing else can supply it (see How Joomla finds the file to render).

Who does what

  • Site builders create and duplicate styles, choose the default one and assign styles to menu items.

  • Administrators decide who may edit templates (the com_templates permissions), turn the position preview on and off, and keep the template files writable only by deployment.

  • Themers write the manifest, the entry files, the parameters and the assets.

  • Developers of extensions only need the template to output the right positions and the component and message includes.

Template styles

Every template has at least one style, created when the template is installed (its title is "<template> - Default"). A style is one row in the #__template_styles table:

Column Meaning

id

The primary key, which menu items reference

template

The template’s folder name, for example cassiopeia_tallpine

client_id

0 for the site, 1 for the administrator

home

0 for an ordinary style, 1 for the default style, or a language tag such as de-DE for the default of one content language

title

The name shown in the Administrator, for example "Tallpine - Default"

inheritable

Copy of the manifest’s <inheritable> value: whether other templates may use this one as a parent

parent

The parent template’s folder name for a child template, otherwise empty

params

The saved parameters, as JSON, built from the <config> fields of the manifest

The inheritable and parent columns exist since Joomla 4.1, the release that introduced child templates. The default data of a fresh 6.2 install contains four rows: Atum (administrator, default), Cassiopeia (site, default, inheritable 1) and Cassiopeia Extended (site, parent set to cassiopeia, not default).

The Template Styles screens

System → Site Template Styles lists the styles with their title, the template, the assigned menu items, the language and the Default column. The toolbar offers Duplicate, Delete and Options, and the row actions open the edit form. System → Site Templates lists the installed templates instead (the extensions), and offers Details and Files (the file editor) and Create Child Template.

A style’s edit form has these tabs:

  • Advanced: the parameters the template declares in its <config> element. The tab names come from the fieldset names of the manifest, so a template with a fieldset advanced shows an Advanced tab.

  • Menu Assignment: a tree of every menu and its items, with a checkbox per item and a toggle per menu. Only non-default styles can be assigned here, because the default style applies to everything that is not assigned.

  • Permissions is a component-level matter handled in Options, not in the style form.

The fields above the tabs are Style Name, Default (a switch, or a content-language selector on a multilingual site) and the template name, which is read-only.

Four rules govern the buttons, and all of them come from StyleModel:

  • There is exactly one default style per client. Setting a style as default (setHome()) first resets the home column of the client’s other rows. A style whose template is disabled cannot be made the default.

  • The default style cannot be deleted, and its Default switch is read-only. Make another style the default first.

  • Duplicate copies a style with all its parameters and an incremented title ("Tallpine - Default (2)"), without menu assignments and never as the default.

  • On a multilingual site (see Multilingual sites) the Default field becomes a language selector, and each content language may have its own default style: the German pages of the club can use a style whose logo reads "Tallpine Wanderverein" while English pages use the first one. home then holds the language tag.

Assigning a style to menu items

The default style is used for every page that does not name another. There are two ways to name another, and the official guide says they "produce the same result":

  1. From the style: open a non-default style and use its Menu Assignment tab. This is better for groups of items.

  2. From the menu item: open the item (Menus → <menu> → <item>) and choose the style in its Template Style field, on the Details tab. This is better for a single item. See Menus and menu items.

Both write the style’s id to the menu item’s template_style_id. Assigning a style to a menu item affects only the page of that item. Pages reached without a matching menu item (a component URL with no menu entry, an error page) fall back to the default, which is why a "member area" look often needs the style assigned to every item of the member menu rather than only to the top one.

The Tallpine club uses three styles:

Style Template Used for

Tallpine - Default

cassiopeia_tallpine

Everything that is not assigned elsewhere (default)

Tallpine - Trail Warden

cassiopeia_tallpine

The items of the Trail Warden menu, with a darker header and the trail-conditions position emphasised

Tallpine - Print

cassiopeia_tallpine

A hidden item used as the target of "print this trail" links

A site builder can also give the home page a separate look without a second template: the User Guide describes assigning a different, non-default style to the Home item only, or adding a Page Class to the Home item and styling it in user.css.

How Joomla chooses the style for a request

The decision is made once per request by SiteApplication, whose template initialisation was refactored into dedicated methods in Joomla 6.1. In order:

  1. Load all styles of enabled site templates (a join of #__template_styles and #__extensions with enabled = 1), cached in the com_templates cache group. The default style, or on a multilingual site the default of the current language, is stored as element 0 of the list.

  2. Take the template_style_id of the active menu item, or of the item named by the Itemid request variable when there is no active item.

  3. If the request contains templateStyle=<id>, that id wins over the menu item’s.

  4. Look the id up in the list; if it is not there (no style, a disabled template) use element 0, the default.

  5. If the request contains template=<name> and an enabled site template of that name with a usable index.php exists, that template’s style replaces the choice.

  6. If the chosen template has no index.php of its own and no parent index.php either, an error message is queued ("could not find the template") and Joomla falls back to the cassiopeia style. Only when Cassiopeia is missing too is an exception thrown.

flowchart TD R[Request] --> A{Active menu item or Itemid?} A -- yes --> M[template_style_id of the item] A -- no --> D0[Default style, element 0] M --> T{templateStyle=id in the URL?} D0 --> T T -- yes --> TS[Use that style id] T -- no --> L{Style in the list of enabled templates?} TS --> L L -- no --> D0b[Default style] L -- yes --> O{template=name in the URL?} D0b --> O O -- yes, valid --> TN[Style of that template] O -- no --> V{index.php found in template or parent?} TN --> V V -- no --> C[Queue error, fall back to cassiopeia] V -- yes --> U[Template and style params used] C --> U

The two request variables are useful tools and small risks:

  • ?templateStyle=14 is handy for previewing an unpublished-looking style on a staging site, but it also means that any visitor can render any page with any style of an enabled template. Keep styles of template experiments on a staging site, not on production.

  • ?template=cassiopeia does the same by template name. Because it is honoured only for enabled templates it cannot load an arbitrary folder, and the name is filtered as a command-type string, but the result is still a second URL for the same content: a page cache or a CDN that keys on the full URL will store a copy, and a crawler that follows a link containing it will index it. Do not generate such links in content.

  • The style is part of what the page cache stores. After changing the default style or an assignment, clear the cache (see Caching and performance); the style list itself is cached and saving a style cleans the com_templates group.

templateDetails.xml

templateDetails.xml is the manifest of a template: the installer reads it to know what to copy and register, and Joomla reads it again at runtime to build the style form and to list the positions. The file name is fixed and the file must sit in the template’s root folder.

The overall shape

<?xml version="1.0" encoding="UTF-8"?>
<extension type="template" client="site" method="upgrade">
    <name>cassiopeia_tallpine</name>
    <version>1.2.0</version>
    <creationDate>2026-10</creationDate>
    <author>Tallpine Hiking Club web team</author>
    <authorEmail>web@tallpine.example</authorEmail>
    <copyright>(C) 2026 Tallpine Hiking Club</copyright>
    <license>GNU General Public License version 2 or later</license>
    <description>TPL_CASSIOPEIA_TALLPINE_XML_DESCRIPTION</description>

    <inheritable>0</inheritable>
    <parent>cassiopeia</parent>
    <namespace path="src">Tallpine\Template\CassiopeiaTallpine</namespace>

    <files>
        <filename>index.php</filename>
        <filename>component.php</filename>
        <filename>error.php</filename>
        <filename>offline.php</filename>
        <filename>joomla.asset.json</filename>
        <filename>templateDetails.xml</filename>
        <folder>html</folder>
        <folder>src</folder>
    </files>

    <media destination="templates/site/cassiopeia_tallpine" folder="media">
        <folder>css</folder>
        <folder>js</folder>
        <folder>scss</folder>
        <folder>images</folder>
    </media>

    <positions>
        <position>trail-conditions</position>
        <position>partner-strip</position>
    </positions>

    <languages folder="language">
        <language tag="en-GB">en-GB/tpl_cassiopeia_tallpine.ini</language>
        <language tag="en-GB">en-GB/tpl_cassiopeia_tallpine.sys.ini</language>
        <language tag="de-DE">de-DE/tpl_cassiopeia_tallpine.ini</language>
        <language tag="de-DE">de-DE/tpl_cassiopeia_tallpine.sys.ini</language>
    </languages>

    <config>
        <fields name="params">
            <fieldset name="advanced">
                <field
                    name="accentColour"
                    type="color"
                    label="TPL_CASSIOPEIA_TALLPINE_ACCENT_LABEL"
                    default="#2f6b3a"
                />
            </fieldset>
        </fields>
    </config>
</extension>

The root <extension> element carries type="template" and the client. method="upgrade" lets a newer package be installed over the older one without uninstalling it first, which would delete the styles; it is the form to use for anything you will ship more than once. Everything below is explained element by element.

Metadata elements

<name> must equal the folder name: it becomes the element in #__extensions and the template in #__template_styles. <version>, <creationDate>, <author>, <authorEmail>, <copyright> and <license> are shown in the Site Templates list and the extension manager. <description> is usually a language constant (TPL_..._XML_DESCRIPTION) defined in the .sys.ini file so that the installer can translate it. The core templates declare creationDate as year and month ("2025-07") and version as "1.0": the version of a core template is not the Joomla version, and a template that is distributed separately should use a version of its own and an update server (see Update servers and the JED).

<files>

<files> lists what the installer copies into templates/<name>/ (or the administrator path), as <filename> and <folder> children. Two things are easy to get wrong:

  • A file that exists in the package but is not listed is not installed. A forgotten <filename>offline.php</filename> is the classic cause of the offline page falling back to the default one only on the live site while it works on the developer’s machine.

  • <folder>html</folder> copies the whole folder, which is where template overrides live (see Template overrides and alternative layouts). Listing only the files you changed keeps a child template honest: the files it does not list are served from the parent. Core child templates, including Cassiopeia Extended, nevertheless list the four entry files.

templateDetails.xml itself is listed so that it remains in the installed folder; the style form reads it from there.

<media>

<media> is a different copy: its destination is a path relative to the site’s media/ folder, and folder names the folder inside the package that holds the files. Stylesheets, scripts, images and fonts that browsers request go here, so that they are served from media/templates/site/cassiopeia_tallpine/, a folder the web server can expose without exposing templates/ PHP files. Cassiopeia uses destination="templates/site/cassiopeia"; an administrator template uses templates/administrator/<name>. Asset URIs in joomla.asset.json are resolved relative to this folder (see Web Asset Manager).

With the public-folder layout of Joomla 6 (see Installation and the public folder) the split between PHP files in templates/ and public files in media/ is what lets an installation keep the former outside the web root. A template that loads its CSS from templates/<name>/css/ instead of media/ breaks on such a site.

<positions>

<positions> lists one <position> per slot name. The list feeds the Position drop-down of the module editor and the Preview Module Positions outlines. It draws nothing: the matching jdoc:include in the entry file does. Two lists must therefore agree, and the module-side effects are described in Modules and positions. Positions used directly by code instead of a modules include (Cassiopeia’s error-403 and error-404, Atum’s cpanel and icon) are listed anyway, with an XML comment saying so, so that they can be chosen in the editor.

The positions of the parent template stay usable in a child template. Cassiopeia Extended repeats the parent’s full list, while the club’s child template declares only the two positions it adds. A position name is free text; the module editor lets you type one that no template declares, which then simply displays nothing until a template outputs it.

<languages>

<languages folder="language"> copies the INI files into the template language folder and registers them. The names follow tpl_<name>.ini (strings of the template at runtime, including the labels of the <config> fields) and tpl_<name>.sys.ini (strings used by the installer and the Administrator lists: the name and description). At runtime HtmlDocument loads the parent template’s language file first and then the child’s, so a child needs to define only the strings it adds or changes. See Language files and internationalisation.

<config>: the style form

Everything under <config><fields name="params"> becomes the form of the style’s parameters. It is the same XML form syntax used elsewhere in Joomla (see Forms and form fields): each <fieldset> becomes a tab, each <field> a control, and the saved values land, as JSON, in the params column of #__template_styles. name="params" is required; the entry files read the values with $this->params->get('name', 'default').

The style form is built from the style’s own template’s manifest. A child template does not inherit the parent’s <config>: Cassiopeia Extended repeats Cassiopeia’s fields and adds its own colour and font settings, and a child that declares nothing gets a style with no template parameters at all. A child that wants the parent’s logo, colour and layout switches must copy those <field> elements, or it must read the parent’s defaults in code.

The 5.1 release added an override for the Menu Assignment tab. A template can place a <fields name="assigned"> element in <config> with a field of type menus, and write the field class in its own namespace, to replace the tab’s default tree with a custom one; the Developer Manual shows a sample MenusField that builds the list with MenusHelper::getMenuLinks(). Most templates never need it.

<inheritable> and <parent>

These two elements, available since Joomla 4.1, express the child-template relationship:

  • <inheritable>1</inheritable> declares that the template may serve as a parent. Cassiopeia and Atum set it to 1; a leaf template such as Cassiopeia Extended or cassiopeia_tallpine sets 0. The Create Child Template button on Site Templates → <template> Details and Files is offered only for templates with inheritable 1. The value is copied to the inheritable column of each style.

  • <parent>cassiopeia</parent> names the parent’s folder. It is copied to the parent column of the styles, and from then on Joomla uses it in four places: file resolution (a missing entry file is taken from the parent), the asset registry (the parent’s joomla.asset.json is registered before the child’s), language loading (the parent’s INI files first) and overrides (the parent’s html/ folder is searched after the child’s).

SiteApplication registers the registries in exactly that order:

// libraries/src/Application/SiteApplication.php (excerpt, simplified)
$template = $this->getTemplate(true);
$wr       = $document->getWebAssetManager()->getRegistry();

if (!empty($template->parent)) {
    $wr->addTemplateRegistryFile($template->parent, $this->getClientId());
}

$wr->addTemplateRegistryFile($template->template, $this->getClientId());

Only one level of inheritance is supported: a template can have one parent and that parent cannot itself be a child. The mechanics, the Create Child Template wizard and what to copy are in Child templates.

<namespace>

Since Joomla 4 an extension may declare a PSR-4 namespace. The Developer Manual’s template skeleton shows <namespace path="src"> with a name of the form company, Templates, template name, and a src folder with field classes and helpers (it is the home of the menus field class mentioned above). The namespace has to be unique, must match the use statements in the template’s files, and the src folder has to be listed under <files>.

Other optional elements

A template manifest may also contain <help> (a key and url for the toolbar’s Help button in the style form, which StyleModel reads), <updateservers> (see Update servers and the JED) and <scriptfile> for an installer script (see Installer scripts and SQL updates). Nothing in <config> is evaluated except the form definition: do not try to put logic in the manifest.

The entry files

The template folder holds up to four PHP files that Joomla itself requires, each chosen by the situation rather than by a menu setting.

File Used when $this is

index.php

Every normal HTML page

HtmlDocument

component.php

The URL has tmpl=component, for example popups, print views and iframes; also the fallback when the template folder is missing

HtmlDocument

error.php

An uncaught exception ends up in the error handler (404, 403, 500)

ErrorDocument, with $this->error and $this->debug

offline.php

The site is offline and the visitor may not log in offline

HtmlDocument

How Joomla finds the file to render

SiteApplication::render() decides the file name, then HtmlDocument::_fetchTemplate() locates it:

  1. The file is <tmpl>.php, where tmpl is the request variable and defaults to index.

  2. If the site is offline and the user lacks the core.login.offline permission, the file becomes offline.php and the response gets 503 Service Temporarily Unavailable.

  3. If tmpl=offline is requested while the site is online, index.php is used instead, so nobody can preview the offline page from the outside.

  4. For errors, ErrorDocument sets the file to error.php itself and sets the HTTP status to the exception’s code (500 when it is outside 400-599).

  5. _fetchTemplate() loads the file from templates/<name>/. If it is not there it tries templates/<parent>/, and as a last resort templates/system/ (using index.php there when even the system folder lacks that file).

flowchart TD S[SiteApplication render] --> F{Offline and no core.login.offline?} F -- yes --> OFF[offline.php, status 503] F -- no --> TM[tmpl request value, default index] TM --> FILE[index.php or component.php] EX[Uncaught exception] --> ERR[error.php, status = code] OFF --> C1{File in templates/name?} FILE --> C1 ERR --> C1 C1 -- yes --> USE[Run it] C1 -- no --> C2{File in templates/parent?} C2 -- yes --> USE C2 -- no --> SYS[templates/system copy] SYS --> USE

The practical consequence for a child template: you override an entry file simply by creating a file with the same name in the child. The child copy replaces the parent file completely; there is no merging. That is why the Tallpine index.php below repeats the parent’s asset setup instead of calling into it.

index.php

index.php is the page. It has three jobs: prepare the document (assets, metadata, parameters), output the HTML skeleton, and place the jdoc:include elements. Joomla runs it with require, so $this is the HtmlDocument and the PHP variables defined in it are plain local variables. The file must start with defined('_JEXEC') or die; (in Joomla 6 JPATH_PLATFORM is no longer defined, so the older defined('JPATH_PLATFORM') guard no longer works unless the Backward Compatibility 6 plugin is on).

The following is the Tallpine child template’s page. It keeps Cassiopeia’s asset presets and adds the two positions, a colour parameter and a sticky notice for members. Run on Joomla 6.2 with cassiopeia_tallpine set as default style it produces a complete page.

<?php

/**
 * @package     Tallpine.Site
 * @subpackage  Templates.cassiopeia_tallpine
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later
 */

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;

/** @var Joomla\CMS\Document\HtmlDocument $this */

$app   = Factory::getApplication();
$input = $app->getInput();
$wa    = $this->getWebAssetManager();
$menu  = $app->getMenu()->getActive();

$sitename  = htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8');
$pageclass = $menu !== null ? $menu->getParams()->get('pageclass_sfx', '') : '';

// Assets of the parent (registered first) and of the child
$wa->usePreset('template.cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr'))
    ->useStyle('template.active.language')
    ->useStyle('template.user')
    ->useScript('template.user')
    ->useStyle('template.tallpine');

// Override 'template.active' so the language stylesheet depends on the right preset
$wa->registerStyle('template.active', '', [], [], ['template.cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]);

// Template style parameters, always with a default
$accent = $this->params->get('accentColour', '#2f6b3a');
$wa->addInlineStyle(':root { --tallpine-accent: ' . htmlspecialchars($accent, ENT_QUOTES, 'UTF-8') . '; }');

$this->setMetaData('viewport', 'width=device-width, initial-scale=1');

$hasLeft  = $this->countModules('sidebar-left', true);
$hasRight = $this->countModules('sidebar-right', true);
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
    <jdoc:include type="metas" />
    <jdoc:include type="styles" />
    <jdoc:include type="scripts" />
</head>

<body class="site <?php echo $input->getCmd('option', '') . ' view-' . $input->getCmd('view', '') . ($pageclass ? ' ' . $pageclass : ''); ?>">
    <header class="header container-header full-width">
        <div class="grid-child">
            <a class="brand-logo" href="<?php echo $this->baseurl; ?>/"><?php echo $sitename; ?></a>
        </div>
        <?php if ($this->countModules('menu', true)) : ?>
            <div class="grid-child container-nav">
                <jdoc:include type="modules" name="menu" style="none" />
            </div>
        <?php endif; ?>
    </header>

    <?php if ($this->countModules('trail-conditions', true)) : ?>
        <aside class="container-trail-conditions" aria-label="<?php echo Text::_('TPL_CASSIOPEIA_TALLPINE_TRAIL_CONDITIONS'); ?>">
            <jdoc:include type="modules" name="trail-conditions" style="none" />
        </aside>
    <?php endif; ?>

    <div class="site-grid<?php echo $hasLeft ? ' has-sidebar-left' : ''; ?><?php echo $hasRight ? ' has-sidebar-right' : ''; ?>">
        <?php if ($hasLeft) : ?>
            <div class="grid-child container-sidebar-left">
                <jdoc:include type="modules" name="sidebar-left" style="card" />
            </div>
        <?php endif; ?>

        <div class="grid-child container-component">
            <jdoc:include type="modules" name="breadcrumbs" style="none" />
            <jdoc:include type="modules" name="main-top" style="card" />
            <jdoc:include type="message" />
            <main>
                <jdoc:include type="component" />
            </main>
            <jdoc:include type="modules" name="main-bottom" style="card" />
        </div>

        <?php if ($hasRight) : ?>
            <div class="grid-child container-sidebar-right">
                <jdoc:include type="modules" name="sidebar-right" style="card" />
            </div>
        <?php endif; ?>
    </div>

    <?php if ($this->countModules('partner-strip', true)) : ?>
        <div class="container-partner-strip full-width">
            <jdoc:include type="modules" name="partner-strip" style="none" />
        </div>
    <?php endif; ?>

    <?php if ($this->countModules('footer', true)) : ?>
        <footer class="container-footer footer full-width">
            <div class="grid-child">
                <jdoc:include type="modules" name="footer" style="none" />
            </div>
        </footer>
    <?php endif; ?>

    <jdoc:include type="modules" name="debug" style="none" />
</body>
</html>

Points to notice, all of them taken from how Cassiopeia itself is written:

  • The three jdoc:include elements in <head> (metas, styles, scripts) are what output everything the Web Asset Manager and the document collected. Leaving one out removes its output silently: a template without type="scripts" loads no JavaScript at all, including the code of core features.

  • type="message" prints the system messages (success notices, errors). Omit it and a failed login shows nothing.

  • type="component" prints the main component output exactly once. It belongs inside a <main> landmark.

  • countModules('name', true) counts modules that have output. Without the second argument a module with empty output still makes the wrapper appear. Several names may be combined, for example countModules('sidebar-left + sidebar-right').

  • The style attribute is the default module style (chrome) for the position. Positions that carry bordered cards in Cassiopeia use card; the others none. See Layouts and module chromes.

  • Everything printed from a parameter is escaped. The accent colour reaches an inline stylesheet, where an unescaped value could break out of the declaration.

  • The assets are named, not echoed. template.tallpine is registered in the child’s joomla.asset.json:

{
  "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
  "name": "cassiopeia_tallpine",
  "version": "1.2.0",
  "description": "Assets of the Tallpine Hiking Club child template.",
  "license": "GPL-2.0-or-later",
  "assets": [
    {
      "name": "template.tallpine",
      "description": "Stylesheet with the Tallpine colours, the trail-conditions banner and the partner strip.",
      "type": "style",
      "uri": "template-tallpine.css",
      "dependencies": [
        "template.cassiopeia.ltr"
      ]
    }
  ]
}

The uri is resolved against media/templates/site/cassiopeia_tallpine/css/. The dependency on the parent’s preset is what guarantees Cassiopeia’s CSS loads before the child’s overrides.

component.php

component.php is the stripped-down page used for tmpl=component URLs: the print view of an article, the modal windows of the Administrator, an iframe target. It should contain metas, styles, scripts, message and component, and nothing that depends on modules or on the site grid. Cassiopeia’s is only the document skeleton with those five includes and the same asset setup as index.php. A child template can omit its own and inherit that one.

<?php

/**
 * @package     Tallpine.Site
 * @subpackage  Templates.cassiopeia_tallpine
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later
 */

defined('_JEXEC') or die;

/** @var Joomla\CMS\Document\HtmlDocument $this */

$wa = $this->getWebAssetManager();

$wa->usePreset('template.cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr'))
    ->useStyle('template.active.language')
    ->useStyle('template.user')
    ->useScript('template.user')
    ->useStyle('template.tallpine');

$wa->registerStyle('template.active', '', [], [], ['template.cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]);
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
    <jdoc:include type="metas" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <jdoc:include type="styles" />
    <jdoc:include type="scripts" />
</head>
<body class="contentpane component <?php echo $this->direction === 'rtl' ? 'rtl' : ''; ?>">
    <jdoc:include type="message" />
    <jdoc:include type="component" />
</body>
</html>

The Tallpine site uses it for the "print this trail" link: the link adds tmpl=component and print=1 to the article URL, and the page loses the header, the sidebars and the footer without any extra template.

error.php

When an exception reaches ExceptionHandler, Joomla builds an ErrorDocument and renders the template’s error.php with $this->error (the Throwable) and $this->debug (true when system debug is on). The HTTP status is the exception’s code. This is the page your visitors see for 404 and 403, so it deserves the template’s navigation.

The caution is in Cassiopeia’s own source: the module renderer does not work properly in an error page because the application is not fully initialised, so it only renders modules when an identity and a language exist, and it uses a variable named $renderModules to decide. The template also supports one module position per error code, error-404, error-403, and so on: if the position error- plus the status code has modules, they are the page content; otherwise the template prints a default explanation. That lets a site builder write the 404 text in a Custom module without touching PHP. The positions are listed in the manifest only so that they appear in the position list.

<?php

/**
 * @package     Tallpine.Site
 * @subpackage  Templates.cassiopeia_tallpine
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later
 */

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\Language\Text;

/** @var Joomla\CMS\Document\ErrorDocument $this */

$app = Factory::getApplication();
$wa  = $this->getWebAssetManager();

$wa->usePreset('template.cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr'))
    ->useStyle('template.active.language')
    ->useStyle('template.tallpine');

$wa->registerStyle('template.active', '', [], [], ['template.cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]);

$errorCode = $this->error->getCode();

// Modules need an identity and a language; both can be missing on an early failure
$renderModules = $app->getIdentity() && $app->getLanguage();
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
    <jdoc:include type="metas" />
    <jdoc:include type="styles" />
    <jdoc:include type="scripts" />
</head>
<body class="site error_site">
    <header class="header container-header full-width">
        <a class="brand-logo" href="<?php echo $this->baseurl; ?>/">Tallpine Hiking Club</a>
        <?php if ($renderModules && $this->countModules('menu')) : ?>
            <jdoc:include type="modules" name="menu" style="none" />
        <?php endif; ?>
    </header>

    <main class="container-component">
        <?php if ($renderModules && $this->countModules('error-' . $errorCode)) : ?>
            <jdoc:include type="message" />
            <jdoc:include type="modules" name="error-<?php echo $errorCode; ?>" style="none" />
        <?php else : ?>
            <h1><?php echo Text::_('TPL_CASSIOPEIA_TALLPINE_ERROR_TITLE'); ?></h1>
            <p><?php echo htmlspecialchars($this->error->getMessage(), ENT_QUOTES, 'UTF-8'); ?></p>
            <p><a class="btn btn-secondary" href="<?php echo $this->baseurl; ?>/"><?php echo Text::_('JERROR_LAYOUT_HOME_PAGE'); ?></a></p>
        <?php endif; ?>

        <?php if ($this->debug) : ?>
            <?php echo $this->renderBacktrace(); ?>
        <?php endif; ?>
    </main>
</body>
</html>

Security consequences of the error page:

  • The exception message and the backtrace are printed only when $this->debug is true. Never output $this->error->getMessage() unescaped (an SQL error may contain query fragments, and a 404 message contains the requested path) and never print the backtrace on a production site; the Error Reporting option in the Global Configuration governs the debug flag as well as the HTTP status text.

  • An error page is also rendered for a failing template. A fatal error inside error.php is the worst place to fail, so keep it simple and test it by requesting a nonexistent URL and by forcing an exception.

The complete treatment of the error, offline and component pages, including the HTTP semantics, is in Error, offline and component pages.

offline.php

When Site Offline is set in the Global Configuration, visitors without the core.login.offline permission (every guest, and by default everyone except Super Users) see offline.php with a 503 status. It must therefore contain its own login form, because the normal login module is not available. Cassiopeia’s version shows the offline image and message (or the default JOFFLINE_MESSAGE), and a form that posts to com_users with the user.login task and a CSRF token, plus the extra buttons that authentication plugins such as passkeys contribute. It loads the template.offline asset from its registry.

<?php

/**
 * @package     Tallpine.Site
 * @subpackage  Templates.cassiopeia_tallpine
 *
 * @copyright   (C) 2026 Tallpine Hiking Club
 * @license     GNU General Public License version 2 or later
 */

defined('_JEXEC') or die;

use Joomla\CMS\Factory;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Router\Route;

/** @var Joomla\CMS\Document\HtmlDocument $this */

$app = Factory::getApplication();
$wa  = $this->getWebAssetManager();

$wa->usePreset('template.cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr'))
    ->useStyle('template.active.language')
    ->useStyle('template.offline')
    ->useStyle('template.tallpine');

$wa->registerStyle('template.active', '', [], [], ['template.cassiopeia.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr')]);

$message = $app->get('offline_message');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
    <jdoc:include type="metas" />
    <jdoc:include type="styles" />
    <jdoc:include type="scripts" />
</head>
<body class="site offline">
    <jdoc:include type="message" />
    <main class="offline-card">
        <h1><?php echo htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8'); ?></h1>
        <p>
            <?php echo $message !== '' && $message !== null
                ? htmlspecialchars($message, ENT_QUOTES, 'UTF-8')
                : Text::_('JOFFLINE_MESSAGE'); ?>
        </p>

        <form action="<?php echo Route::_('index.php', true); ?>" method="post" id="form-login">
            <label for="username"><?php echo Text::_('JGLOBAL_USERNAME'); ?></label>
            <input name="username" id="username" type="text" autocomplete="username">
            <label for="password"><?php echo Text::_('JGLOBAL_PASSWORD'); ?></label>
            <input name="password" id="password" type="password" autocomplete="current-password">
            <button type="submit" class="btn btn-primary"><?php echo Text::_('JLOGIN'); ?></button>
            <input type="hidden" name="option" value="com_users">
            <input type="hidden" name="task" value="user.login">
            <input type="hidden" name="return" value="<?php echo base64_encode(Route::_('index.php', false)); ?>">
            <?php echo HTMLHelper::_('form.token'); ?>
        </form>
    </main>
</body>
</html>

Pitfalls specific to this file: the offline page is rendered without the module positions of the normal page; the offline message is administrator-supplied text that Cassiopeia prints unescaped (the safehtml filter has already stripped dangerous markup when the configuration was saved); and because the form posts to the real login task, a template author who removes the CSRF token (form.token) makes the site’s offline login fail with "invalid token". The Global Configuration side of offline mode is in Global configuration.

jdoc:include reference

The jdoc:include element is not HTML. It is a placeholder that HtmlDocument replaces after the template has run, by calling a renderer chosen by the type attribute.

type Output Attributes

metas

Charset, viewport, robots, description, generator, canonical and other <meta> and <link> elements of the document head

none

styles

The stylesheets, inline styles and preloads the Web Asset Manager collected

none

scripts

The scripts, inline scripts and script options (joomla-script-options)

none

message

The system message queue

none

component

The output of the component that handled the request

none

modules

Every module published to a position

name (position), style (module chrome)

module

A single module, found by its module type

name (for example mod_login), title, style

head

Legacy single element combining metas, styles and scripts

none

Two cautions. First, jdoc:include elements are replaced by simple text substitution, so a style attribute that names a chrome the template does not define falls back to the default chrome rather than failing, and a typo shows up as unstyled modules rather than as an error. Second, the position attribute is a plain string: name="trail-conditions" and name="trail_conditions" are two different positions.

Template code should avoid echoing <?php echo $this->getBuffer('modules', 'x'); ?> unless it needs the output as a string; the countModules() check plus jdoc:include is the supported pattern and keeps the render order Joomla expects.

Template parameters

Parameters make a template configurable by non-programmers and are the reason styles exist. They are defined in the manifest ([config: the style form]), stored per style, and read in the entry files.

Defining parameters

The following adds three fields to the Tallpine manifest: a colour, a switch that shows or hides a seasonal notice, and a text field that is shown only when the switch is on. The showon attribute links the second field to the first, the way Cassiopeia hides its logo and title fields when the brand switch is off.

<config>
    <fields name="params">
        <fieldset name="advanced" label="TPL_CASSIOPEIA_TALLPINE_FIELDSET_LABEL">
            <field
                name="accentColour"
                type="color"
                label="TPL_CASSIOPEIA_TALLPINE_ACCENT_LABEL"
                default="#2f6b3a"
                validate="color"
            />

            <field
                name="seasonNotice"
                type="radio"
                label="TPL_CASSIOPEIA_TALLPINE_SEASON_NOTICE_LABEL"
                layout="joomla.form.field.radio.switcher"
                default="0"
                filter="integer"
                >
                <option value="0">JNO</option>
                <option value="1">JYES</option>
            </field>

            <field
                name="seasonNoticeText"
                type="text"
                label="TPL_CASSIOPEIA_TALLPINE_SEASON_NOTICE_TEXT_LABEL"
                default=""
                filter="string"
                showon="seasonNotice:1"
            />

            <field
                name="logoFile"
                type="media"
                schemes="http,https,ftp,ftps,data,file"
                validate="url"
                relative="true"
                default=""
                label="TPL_CASSIOPEIA_TALLPINE_LOGO_LABEL"
            />
        </fieldset>
    </fields>
</config>

The labels use constants defined in tpl_cassiopeia_tallpine.ini:

TPL_CASSIOPEIA_TALLPINE_FIELDSET_LABEL="Tallpine settings"
TPL_CASSIOPEIA_TALLPINE_ACCENT_LABEL="Accent colour"
TPL_CASSIOPEIA_TALLPINE_SEASON_NOTICE_LABEL="Show seasonal notice"
TPL_CASSIOPEIA_TALLPINE_SEASON_NOTICE_TEXT_LABEL="Notice text"
TPL_CASSIOPEIA_TALLPINE_LOGO_LABEL="Logo"
TPL_CASSIOPEIA_TALLPINE_TRAIL_CONDITIONS="Trail conditions"
TPL_CASSIOPEIA_TALLPINE_ERROR_TITLE="This trail does not exist"

Reading and using parameters

$this->params is a Joomla\Registry\Registry built from the style’s params JSON. Always pass a default, because a style created before the field existed has no value for it, and never trust a stored value in markup:

<?php
// In index.php, after the asset setup
$seasonNotice = (int) $this->params->get('seasonNotice', 0);
$noticeText   = trim((string) $this->params->get('seasonNoticeText', ''));

if ($seasonNotice && $noticeText !== '') {
    echo '<div class="alert alert-info tallpine-season-notice" role="status">'
        . htmlspecialchars($noticeText, ENT_QUOTES, 'UTF-8')
        . '</div>';
}

// A logo selected in the media field is a relative path with an optional #joomlaImage suffix
$logo = $this->params->get('logoFile');

if ($logo) {
    $logo = HTMLHelper::_('cleanImageURL', $logo)->url;
}

Notes on parameters:

  • The filter attribute controls what is stored (integer, string, options, raw, unset …​). validate rejects bad input before saving. Use filter="integer" or validate="options" on switches and lists so a hand-edited POST cannot save an arbitrary value. Cassiopeia’s own fields use both.

  • A media field returns a path relative to the site root, to which Joomla may append #joomlaImage://... metadata. Clean it before use, as shown, and pass it through htmlspecialchars if you print it.

  • Raw HTML in a parameter (an "analytics snippet" field, for example) executes for every visitor and is an administrator-to-visitor XSS vector. Do not offer filter="raw" fields to editors; use a Custom module with its own access control instead.

  • Parameters belong to the style, not the template. Duplicating a style duplicates its parameters, and updating the template package does not overwrite saved values. When an update adds a field, the entry file must tolerate its absence.

  • A child template has its own parameters. Reading $this->params->get('colorName') in the child only works if the child’s manifest declares colorName (Cassiopeia’s <config> is not inherited).

  • On the site, the active template’s parameters are also available as $app->getTemplate(true)->params, which is how a module or a plugin reads them.

Inspecting styles with SQL

For a staging site, a quick query shows which style each client uses and what it stores:

SELECT id, template, client_id, home, title, parent, params
FROM `#__template_styles`
WHERE client_id = 0
ORDER BY home DESC, id;

Edit the table by hand only to recover a broken site (a default style pointing at a deleted template, for example, which sends every visitor to the Cassiopeia fallback with an error message). Prefer the Administrator, which also cleans the cache.

Previewing positions

A position is an invisible slot, so the fastest way to learn a template, or to find out why a module does not show, is to draw all of them.

  1. Open System → Site Template Styles and click Options.

  2. Set Preview Module Positions to Enabled and save. (The same switch exists under System → Site Templates → Options.) The setting is the template_positions_display parameter of com_templates, default Disabled.

  3. Visit the site with ?tp=1 appended, or &tp=1 when the URL already has a query string, for example https://tallpine.example/?tp=1.

Each position the template outputs then appears outlined and labelled with its name and the module style in use. The position list in the manifest is not enough by itself: a position declared in <positions> but not output by index.php stays invisible, which is the quickest test for the two lists disagreeing. The same screen holds the other Templates: Options: upload limit (default 10 MB), the allowed image, source and font extensions for the template file editor and the way the file Compare view shows differences (side by side by default).

Two cautions. The switch is global, so while it is on any visitor can add ?tp=1 and see the layout and the names of every module position, so turn it off after use and on production sites keep it off. And the outline style wraps real modules too, so a page previewed with tp=1 is not a faithful rendering. The mechanics (a dummy module is prepended to every empty position) and the interplay with module chromes are in Modules and positions.

Common errors and pitfalls

  • "The module is published but I cannot see it." The template does not output its position, the position is spelled differently in the module and in index.php, or the style assigned to the menu item uses a different template than the one you edited. Check the style first (the Template Style column of the menu item), then use ?tp=1.

  • A new index.php has no effect. A child template’s files are installed from the package: on a server the folder was not updated, the file was not listed under <files>, or the cache holds the old page. Clear the cache; look at the actual folder on disk.

  • The default style cannot be deleted or unassigned. This is intended; make another style the default first.

  • All pages look the same after assigning a style to one menu item. The assignment is per item. Children of a menu item and component URLs without an item use the default.

  • Parameters disappear after an update. Updates keep the params column; if a field’s name was changed in the new manifest, the saved value is no longer read. Treat a field name as a stored key.

  • The site shows "template not found" and falls back to Cassiopeia. The style points to a folder that no longer exists, or the folder lacks index.php and has no parent. Reinstall the template or repair the row.

  • Styles and scripts missing. The head does not contain the three includes, or the asset is never use`d. The page source shows nothing from the template’s CSS when `$wa->useStyle() is never called.

  • Fatal error on a Joomla 6 site after copying an old template. Old templates call removed APIs (for example JFactory, JText and JHtml aliases, JPATH_PLATFORM guards, $doc->addStyleSheet(), jQuery assumptions). Core keeps the legacy class aliases only through the Backward Compatibility 6 plugin. Move to the namespaced classes and the Web Asset Manager.

  • tmpl=component shows a styled page that is too large. The template’s component.php is missing, so the parent’s or templates/system's is used; add a lean one.

  • Offline login fails. The CSRF token or the hidden option and task fields are missing from offline.php.

  • Wrong HTML language. Printing a hard-coded lang="en" on a multilingual site; use $this->language and $this->direction.

Security considerations

  • Restrict Create, Edit and Delete on com_templates and the file editor (Details and Files and Customise). The file editor writes PHP into templates/: a stolen editor account becomes remote code execution. Many production sites remove the permission entirely and deploy template files from version control, and the hardening guidance of Security hardening, MFA and passkeys recommends it.

  • Every defined('_JEXEC') or die; guard prevents the file from being executed directly by URL. Keep it in every PHP file of the template, including layout overrides.

  • Escape everything echoed from $this->params, from the request and from the error object. Template parameters are edited by administrators, but a lower-privileged account that can edit a style (the permission is separate from Super User) can still inject script into every page if the template prints parameters raw.

  • Do not leave debugging in a template: var_dump, the backtrace of the error page and the preview switch are information leaks.

  • Third-party templates run with the same rights as core. Install only from the JED or from a vendor you can audit and keep them updated; see Template frameworks and page builders.

  • ?template= and ?templateStyle= are request-controlled, as described in How Joomla chooses the style for a request.

Accessibility of the generated markup is the responsibility of the template: the landmarks (header, nav, main, footer), the order of positions, visible focus and the colour contrast of the accent parameter. The general rules are in Accessibility, and the Joomla-specific colour modes and right-to-left support in Colour modes, RTL and accessibility.

Version notes

  • Joomla 4.0: Cassiopeia and Atum replace Beez3, Protostar and Hathor; templates use joomla.asset.json and the Web Asset Manager instead of addStyleSheet() and JHtml::_('jquery.framework').

  • Joomla 4.1: child templates, with the <inheritable> and <parent> manifest elements and the matching inheritable and parent columns of #__template_styles.

  • Joomla 5.0: the ES5 assets were removed from core, so template scripts must be written for modern browsers.

  • Joomla 5.1: an override for the Menu Assignment tab through a menus field in <config>.

  • Joomla 6.0: the cassiopeia_extended child template ships in core; JPATH_PLATFORM is no longer defined and the legacy J* aliases move to the Backward Compatibility 6 plugin.

  • Joomla 6.1: the template initialisation of SiteApplication is split into initialiseTemplate(), getTemplates() and related methods (the order described above); a "version": "nocache" asset option arrives (see Web Asset Manager).

  • Joomla 6.2: current; no change to the template manifest or entry files.

Corrections to older books and tutorials

Older books, such as Joomla! 4 Masterclass (2023) and Developing Extensions for Joomla! 5 (2023), and many tutorials describe the same ideas as this page but need the following corrections for a 2026 site:

  • Child templates are described as "brand new". They have existed since Joomla 4.1, and since 6.0 core itself ships one (cassiopeia_extended); treat them as the standard way to customise Cassiopeia rather than an experiment.

  • Template examples built on JFactory, JHtml, JText or $doc->addScript() and jQuery need rewriting with the namespaced classes and the Web Asset Manager; the aliases are no longer defined on a new Joomla 6 install.

  • Links to docs.joomla.org/J4.x pages and launch.joomla.org are dead or obsolete. The current documentation is the Joomla User Guide at guide.joomla.org and the Developer Manual at manual.joomla.org.

  • Joomla 4 is end of life (October 2025) and 5.x receives security fixes only; the examples there are for versions that are no longer the target of a new project.

  • Several pages of the Manual about templates (the generic "Template" page, colour schemes) are still unfinished; the templates/cassiopeia source tree is the authoritative example, which is why this page cites it.

  • The manual and older guides describe sixteen Cassiopeia positions; the manifest also lists error-403 and error-404, used by error.php.