Building a Joomla Template from Scratch

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 site template is a folder of PHP, XML, CSS and language files that turns the output of components and modules into a complete HTML page. Most sites never need to write one from nothing: a child template of Cassiopeia changes colours, fonts and layouts with a handful of files. Building a template from scratch is still worth doing once, because it shows exactly which files Joomla reads, which statements it replaces and which conventions the Web Asset Manager expects, and because some projects (a design system delivered by an agency, an intranet with its own markup) really do need a template that inherits from nothing. This page builds the smallest useful standalone template, tallpine_minimal, for the Tallpine Hiking Club: a manifest, a main layout with jdoc statements, assets registered through joomla.asset.json, module positions, an error page, an offline page, a component-only layout and language files. It then covers three ways to build SCSS, an accessibility checklist, and how to package and install the template. It was checked against the Developer Manual, the Joomla User Guide and the 6.2 source tree on 2026-10-30.

The behaviour described is that of Joomla 6.2 (current), which requires PHP 8.3 or newer. Template manifests, jdoc statements and the Web Asset Manager work the same way in Joomla 4.x and 5.x, so most of this page also applies to Joomla 5.4 (security fixes only); where 6.x differs, the text says so. The audience is the themer and the developer. Template styles, the Templates manager and the choice between template and child template are explained in Templates and template styles; the shipped template that this page uses as its model is described in Cassiopeia and Cassiopeia Extended.

What a template is made of

A site template is installed in two places. The PHP and XML files go to templates/<name>/; static files (CSS, JavaScript, images) go to media/templates/site/<name>/, the folder the web server serves directly. Joomla treats the template name as the folder name, so tallpine_minimal must match the folder in templates/ and the destination of the media element in the manifest.

File Purpose Required

templateDetails.xml

Manifest: metadata, file lists, media folders, positions, language files and style parameters

yes

index.php

Main page layout for the front end; contains the jdoc statements

yes

joomla.asset.json

Registers the template’s stylesheets and scripts in the Web Asset Manager

recommended

component.php

Bare layout used when the request asks for tmpl=component (print views, modal dialogs)

optional

error.php

Layout used when an exception such as 404 or 403 is displayed

optional

offline.php

Layout used when Global Configuration sets the site offline

optional

language/<tag>/tpl_<name>.ini, .sys.ini

Translatable strings for the front end and for the installer and administrator lists

recommended

html/

Template overrides

optional

If error.php, offline.php or component.php are missing, Joomla falls back to the versions in templates/system, which are deliberately plain. A minimal template can therefore ship only templateDetails.xml and index.php; the sections below add the rest because a real site notices their absence.

Joomla resolves the active template for a request from the template style assigned to the menu item (or the default style). Template styles, and the way the same template can have several styles with different parameters, are in Templates and template styles. The flow from request to page is:

flowchart LR R[Request] --> S[Template style<br/>chosen from menu item] S --> I[index.php of the template] I --> W[Web Asset Manager<br/>joomla.asset.json] I --> J[jdoc statements] J --> M[metas, styles, scripts] J --> C[component output] J --> P[modules per position] J --> G[system messages] M --> H[Final HTML document] C --> H P --> H G --> H

The standalone template has this file layout (the media folder is moved to media/templates/site/tallpine_minimal by the installer):

tpl_tallpine_minimal/
  templateDetails.xml
  index.php
  error.php
  offline.php
  component.php
  joomla.asset.json
  language/
    en-GB/
      tpl_tallpine_minimal.ini
      tpl_tallpine_minimal.sys.ini
    de-DE/
      tpl_tallpine_minimal.ini
      tpl_tallpine_minimal.sys.ini
  media/
    css/
      template.css
    js/
      template.js
    images/
      logo.svg

The manifest: templateDetails.xml

The manifest is an <extension type="template" client="site"> document. It names the template, lists every file that the installer must copy, declares module positions and language files, and describes the parameters of the template style form. The Developer Manual page on templateDetails.xml documents the elements and is, together with the Cassiopeia manifest in the source tree, the reference for this section.

<?xml version="1.0" encoding="UTF-8"?>
<extension type="template" client="site">
    <name>tallpine_minimal</name>
    <version>1.0.0</version>
    <creationDate>2026-10</creationDate>
    <author>Tallpine Hiking Club</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_TALLPINE_MINIMAL_XML_DESCRIPTION</description>
    <inheritable>1</inheritable>
    <files>
        <filename>component.php</filename>
        <filename>error.php</filename>
        <filename>index.php</filename>
        <filename>joomla.asset.json</filename>
        <filename>offline.php</filename>
        <filename>templateDetails.xml</filename>
    </files>
    <media destination="templates/site/tallpine_minimal" folder="media">
        <folder>css</folder>
        <folder>js</folder>
        <folder>images</folder>
    </media>
    <positions>
        <position>header</position>
        <position>menu</position>
        <position>breadcrumbs</position>
        <position>sidebar</position>
        <position>main-top</position>
        <position>main-bottom</position>
        <position>footer</position>
        <position>debug</position>
        <position>error-403</position>
        <position>error-404</position>
    </positions>
    <languages folder="language">
        <language tag="en-GB">en-GB/tpl_tallpine_minimal.ini</language>
        <language tag="en-GB">en-GB/tpl_tallpine_minimal.sys.ini</language>
        <language tag="de-DE">de-DE/tpl_tallpine_minimal.ini</language>
        <language tag="de-DE">de-DE/tpl_tallpine_minimal.sys.ini</language>
    </languages>
    <config>
        <fields name="params">
            <fieldset name="advanced">
                <field
                    name="fluidContainer"
                    type="radio"
                    layout="joomla.form.field.radio.switcher"
                    default="0"
                    filter="integer"
                    label="TPL_TALLPINE_MINIMAL_FLUID_LABEL"
                    >
                    <option value="0">JNO</option>
                    <option value="1">JYES</option>
                </field>
                <field
                    name="siteTitle"
                    type="text"
                    default=""
                    filter="string"
                    label="TPL_TALLPINE_MINIMAL_TITLE_LABEL"
                />
            </fieldset>
        </fields>
    </config>
</extension>

Points that matter in practice:

  • name is the identifier. It must equal the template folder name, use lowercase letters, digits and underscores, and never change between versions, because template styles, menu assignments and overrides refer to it.

  • description and <position> labels are language keys. The installer and the Templates manager look the keys up in the .sys.ini file, so they show real text rather than TPL_TALLPINE_MINIMAL_XML_DESCRIPTION.

  • inheritable set to 1 allows another template to name this one as parent. Cassiopeia sets it. Leave it out if you do not want children; the details are in Joomla child templates.

  • files and media are exhaustive. A file that is not listed is not copied. The installer lists folders such as html with a <folder> element. Forgetting a file is the most common cause of a template that works from the working tree and breaks after installation.

  • media destination places the folder under media/templates/site/<name>. The uri values in joomla.asset.json are resolved relative to that folder’s css and js subfolders.

  • Parameters declared under <config> appear on the Advanced tab of the template style and are read in PHP with $this->params->get(). Give every field a filter so that stored values are cleaned; integer for switches, string for plain text. Treat each value as untrusted when you echo it.

  • Since Joomla 5.1 the Menu Assignment tab of a template style can be overridden in the config section with a custom form field, according to the Manual page; the minimal template does not need it.

  • Joomla stores the list of positions in the manifest only to offer names in the module editor. A module can still be assigned to a position the manifest does not list, by typing the name; the template must then print that position.

The main layout: index.php

index.php runs in the context of the Joomla\CMS\Document\HtmlDocument object, so $this gives access to the language ($this->language), the text direction ($this->direction), the base URL ($this->baseurl), the style parameters ($this->params), the module counter ($this->countModules()) and the Web Asset Manager ($this->getWebAssetManager()). The file is plain PHP mixed with HTML. Joomla replaces the <jdoc:include> placeholders after the template has run, so they are not PHP and are never executed.

<?php

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

defined('_JEXEC') or die;

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

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

$app      = Factory::getApplication();
$sitename = htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8');
$wa       = $this->getWebAssetManager();

// Enable the assets registered in joomla.asset.json
$wa->usePreset('template.tallpine_minimal.' . ($this->direction === 'rtl' ? 'rtl' : 'ltr'))
    ->useStyle('template.active.language')
    ->useStyle('template.user')
    ->useScript('template.user');

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

$wrapper  = $this->params->get('fluidContainer') ? 'wrapper-fluid' : 'wrapper-static';
$title    = $this->params->get('siteTitle') ?: $sitename;
$hasAside = $this->countModules('sidebar', 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 $wrapper . ($hasAside ? ' has-sidebar' : ''); ?>">
    <a class="skip-link" href="#content"><?php echo Text::_('TPL_TALLPINE_MINIMAL_SKIP_TO_CONTENT'); ?></a>

    <header class="site-header">
        <a class="brand" href="<?php echo $this->baseurl; ?>/"><?php echo htmlspecialchars($title, ENT_QUOTES, 'UTF-8'); ?></a>
        <?php if ($this->countModules('header', true)) : ?>
            <jdoc:include type="modules" name="header" style="none" />
        <?php endif; ?>
        <?php if ($this->countModules('menu', true)) : ?>
            <nav aria-label="<?php echo Text::_('TPL_TALLPINE_MINIMAL_MAIN_NAVIGATION'); ?>">
                <jdoc:include type="modules" name="menu" style="none" />
            </nav>
        <?php endif; ?>
    </header>

    <div class="site-grid">
        <div class="content-area">
            <jdoc:include type="modules" name="breadcrumbs" style="none" />
            <jdoc:include type="modules" name="main-top" style="none" />
            <jdoc:include type="message" />
            <main id="content" tabindex="-1">
                <jdoc:include type="component" />
            </main>
            <jdoc:include type="modules" name="main-bottom" style="none" />
        </div>
        <?php if ($hasAside) : ?>
            <aside class="sidebar" aria-label="<?php echo Text::_('TPL_TALLPINE_MINIMAL_SIDEBAR'); ?>">
                <jdoc:include type="modules" name="sidebar" style="none" />
            </aside>
        <?php endif; ?>
    </div>

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

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

The jdoc statements

Statement What Joomla puts there

<jdoc:include type="metas" />

Character set, viewport and description meta tags, robots, generator, canonical and alternate links

<jdoc:include type="styles" />

The <link> and <style> elements for every stylesheet enabled in the Web Asset Manager

<jdoc:include type="scripts" />

The <script> elements, including the JSON script options and the import map

<jdoc:include type="head" />

All three of the above at once; still valid and used by templates/system/component.php

<jdoc:include type="message" />

System messages (errors, success notices) rendered by the message layout

<jdoc:include type="component" />

The output of the component selected by the menu item or the URL

<jdoc:include type="modules" name="x" style="y" />

Every published module assigned to position x and to this page, rendered with module chrome y

<jdoc:include type="module" name="mod_login" />

One named module (the module type, singular); rarely needed

Splitting the head into metas, styles and scripts, as Cassiopeia does, lets the template place scripts differently from styles and is the current practice. Older tutorials, and the books Developing Extensions for Joomla! 5 (2023) and Joomla! 4 Masterclass (2023) in parts, still show a single type="head"; it works, but a new template gains nothing from it.

The style attribute selects a module chrome, the wrapper around each module: none outputs the bare module, html5 wraps it in a <div> with the title in a heading, and Cassiopeia’s card produces Bootstrap card markup. The chromes, and how to write your own, are covered in Layouts and module chromes. Positions, module assignment and the {loadposition} content tag are in Modules and positions.

Three details prevent frequent mistakes:

  • Wrap optional positions in countModules(). $this->countModules('sidebar', true) returns the number of published modules assigned to the position, so the markup around an empty position is not printed. Cassiopeia passes true as a second argument for the positions that wrap markup, and the same call shape is used here. Without the guard the page contains empty landmarks that screen readers announce.

  • Always include message and component. A template that omits message swallows every error the user should see, including failed logins and validation errors; one that omits component shows nothing from the site.

  • Escape what is not trusted. Values from $this->params and $app->get() are configuration, but they come from forms that administrators edit, so print them with htmlspecialchars(). Never print request input without escaping it.

To see the names of positions on a live page, enable Preview Module Positions in the Templates options and append ?tp=1 to a front-end URL; every position shows as an outlined box with its name, which is the quickest way to check that the manifest and index.php agree.

The layout above already contains the structural pieces that accessibility audits ask for first: a skip link before the header, a single <main> that is the target of the skip link, <nav> and <aside> landmarks with accessible names, and lang and dir attributes on <html>. The full checklist is further down.

Assets through the Web Asset Manager

Joomla 4 and later load stylesheets and scripts through the Web Asset Manager (WAM). A template does not print <link> or <script> tags; it registers assets in a JSON file and enables them in PHP, and WAM resolves dependencies, adds cache-busting versions and prints the tags at the styles and scripts statements. The WAM page of this sub-section is Web Asset Manager; this section shows what a template needs.

{
  "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
  "name": "tallpine_minimal",
  "version": "1.0.0",
  "description": "Assets of the Tallpine Minimal site template.",
  "license": "GPL-2.0-or-later",
  "assets": [
    {
      "name": "template.tallpine_minimal.ltr",
      "description": "Stylesheet for left-to-right languages.",
      "type": "style",
      "uri": "template.css"
    },
    {
      "name": "template.tallpine_minimal.rtl",
      "description": "Stylesheet for right-to-left languages.",
      "type": "style",
      "uri": "template-rtl.css"
    },
    {
      "name": "template.tallpine_minimal",
      "description": "JavaScript of the template.",
      "type": "script",
      "uri": "template.js",
      "attributes": { "type": "module" },
      "dependencies": ["core"]
    },
    {
      "name": "template.tallpine_minimal.ltr",
      "type": "preset",
      "dependencies": [
        "template.tallpine_minimal.ltr#style",
        "template.tallpine_minimal#script"
      ]
    },
    {
      "name": "template.tallpine_minimal.rtl",
      "type": "preset",
      "dependencies": [
        "template.tallpine_minimal.rtl#style",
        "template.tallpine_minimal#script"
      ]
    },
    {
      "name": "template.user",
      "description": "Optional site-specific CSS written by the administrator.",
      "type": "style",
      "uri": "user.css",
      "weight": 500
    },
    {
      "name": "template.user",
      "description": "Optional site-specific JavaScript.",
      "type": "script",
      "uri": "user.js",
      "weight": 500
    }
  ]
}

The keys used:

  • name is the registry key; the same name can be used once per type, so a style and a script may share a name, as template.user does here. Presets and styles with the same name are told apart by the #style and #script suffix in dependencies.

  • type is style, script or preset. A preset enables a group of assets in one usePreset() call, which Cassiopeia uses to switch between its LTR and RTL stylesheets.

  • uri is relative to the template’s media folder and omits the css or js subfolder: template.css is media/templates/site/tallpine_minimal/css/template.css. A full URL is also accepted for external files, and a missing file referenced by a relative uri is silently skipped, so check the browser’s network panel after the first load.

  • dependencies lists assets that must load first. core is Joomla’s JavaScript core, needed by any script that uses Joomla.*.

  • attributes are copied onto the tag; {"type": "module"} loads the file as an ES module, and defer or async are also possible. ES5 builds were dropped in Joomla 5.0, so write modern JavaScript.

  • weight orders assets that are otherwise equal. A larger weight loads later, which is why a site’s own user.css can override the template.

  • The file is read last among the registries: media/vendor, media/system, media/legacy, the component’s registry and then the active template’s, so a template can override an asset registered earlier by registering one of the same name. Modules and plugins must register their own registry file with addRegistryFile(); they are not found automatically.

  • Joomla 6.1 added "version": "nocache", which makes Joomla append no version query to an asset URL, useful while developing. Without it, the version of the registry file or the file’s hash is used for cache busting; see Caching and performance.

In index.php the template then writes usePreset(), useStyle() and useScript() as shown above. The two assets template.active.language (language-specific CSS such as language/de-DE/de-DE.css) and template.user (the administrator’s user.css and user.js) are the same hooks Cassiopeia offers, and they are what lets a child template add CSS without touching its parent.

Older code, and both books, call $doc->addStyleSheet(), HTMLHelper::_('stylesheet', ...) or print <script> tags in the template, and load jQuery through HTMLHelper::_('jquery.framework'). Those calls are legacy; since Joomla 4 core does not depend on jQuery, and a template that bypasses the WAM loses dependency ordering, cache busting and the ability for child templates and administrators to replace assets. Joomla 6.0 also moved the J* aliases such as JFactory and JHtml into the Backward Compatibility 6 plugin, off by default on new installs, so import the namespaced classes with use as the examples here do.

A minimal stylesheet to go with the manifest:

:root {
  --tp-green: #1f5e3b;
  --tp-ink: #1d2b22;
  --tp-bg: #f7f9f5;
}

body {
  margin: 0;
  color: var(--tp-ink);
  background: var(--tp-bg);
  font: 1rem/1.6 system-ui, sans-serif;
}

.skip-link {
  position: absolute;
  inset-inline-start: 0.5rem;
  inset-block-start: -4rem;
  padding: 0.5rem 1rem;
  background: #fff;
}

.skip-link:focus {
  inset-block-start: 0.5rem;
}

.wrapper-static .site-header,
.wrapper-static .site-grid,
.wrapper-static .site-footer {
  max-width: 72rem;
  margin-inline: auto;
}

.site-grid {
  display: grid;
  gap: 2rem;
  padding: 1rem;
}

.has-sidebar .site-grid {
  grid-template-columns: minmax(0, 1fr) 18rem;
}

@media (max-width: 48rem) {
  .has-sidebar .site-grid {
    grid-template-columns: 1fr;
  }
}

a:focus-visible {
  outline: 3px solid currentcolor;
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  * {
    animation: none !important;
    transition: none !important;
  }
}

Logical properties such as inset-inline-start and margin-inline make one stylesheet work for both text directions; the RTL file can then be nearly empty, and the template’s right-to-left support is covered in the Manual page "Supporting Right to Left Languages" and in Colour modes, RTL and accessibility.

The other layouts

component.php

When a URL carries tmpl=component, Joomla uses component.php instead of index.php. Print views, the Media Manager dialogs and some modal forms rely on it. The templates/system/component.php file is the model: only the head, the messages and the component.

<?php

defined('_JEXEC') or die;

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

$wa = $this->getWebAssetManager();
$wa->useStyle('template.tallpine_minimal.ltr');
?>
<!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="contentpane component">
    <jdoc:include type="message" />
    <main>
        <jdoc:include type="component" />
    </main>
</body>
</html>

error.php

When an uncaught exception reaches the site, Joomla renders error.php. The $this object is an ErrorDocument whose error property holds the exception. Since Joomla 5.0, Cassiopeia lets editors fill the error-403 and error-404 positions with modules and prints them instead of the default message; your own template can do the same, as the Manual page "Custom Error Pages" shows.

<?php

defined('_JEXEC') or die;

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

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

$app       = Factory::getApplication();
$errorCode = $this->error->getCode();

// The module renderer does not work when the application failed to initialise
$renderModules = $app->getIdentity() && $app->getLanguage();

$this->getWebAssetManager()->useStyle('template.tallpine_minimal.ltr');
$this->setMetaData('viewport', 'width=device-width, initial-scale=1');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
    <jdoc:include type="metas" />
    <jdoc:include type="styles" />
</head>
<body class="site error-page">
    <main class="container">
        <?php if ($renderModules && $this->countModules('error-' . $errorCode)) : ?>
            <jdoc:include type="modules" name="error-<?php echo $errorCode; ?>" style="none" />
        <?php else : ?>
            <h1><?php echo Text::_('TPL_TALLPINE_MINIMAL_ERROR_HEADING'); ?></h1>
            <p><?php echo htmlspecialchars($this->error->getMessage(), ENT_QUOTES, 'UTF-8'); ?></p>
            <p><a href="<?php echo $this->baseurl; ?>/"><?php echo Text::_('TPL_TALLPINE_MINIMAL_BACK_HOME'); ?></a></p>
        <?php endif; ?>
    </main>
</body>
</html>

Pitfalls specific to error pages:

  • The exception can be thrown before the application, the database or the modules are available, for example when the database is down. A module position then cannot be rendered, and Cassiopeia guards its module output so that an initialisation failure does not turn into a second error. Keep the default branch free of module and database dependencies and do not call a position unconditionally.

  • $this->error->getMessage() can contain file paths or SQL when debug is on. Print it escaped, and show a backtrace only when the site is in debug mode, as Cassiopeia does. Never show raw messages to visitors on a production site.

  • The error page is served with the error’s HTTP status; keep it that way. A custom 404 page that returns 200 is a "soft 404" for search engines.

  • Error pages share responsibilities with the offline and component pages; a fuller treatment is in Error, offline and component pages.

offline.php

With Site Offline set to Yes in Global Configuration, Joomla renders offline.php to visitors who are not logged in to the administrator. The system template’s version prints the site name, an optional image and a message chosen by the Display Offline Message setting, then a login form. A minimal version only needs the message:

<?php

defined('_JEXEC') or die;

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

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

$app = Factory::getApplication();
$this->getWebAssetManager()->useStyle('template.tallpine_minimal.ltr');
$message = $app->get('display_offline_message', 1) == 1 && $app->get('offline_message')
    ? $app->get('offline_message')
    : Text::_('JOFFLINE_MESSAGE');
?>
<!DOCTYPE html>
<html lang="<?php echo $this->language; ?>" dir="<?php echo $this->direction; ?>">
<head>
    <jdoc:include type="metas" />
    <jdoc:include type="styles" />
</head>
<body class="site offline">
    <main class="container">
        <h1><?php echo htmlspecialchars($app->get('sitename'), ENT_QUOTES, 'UTF-8'); ?></h1>
        <jdoc:include type="message" />
        <p><?php echo $message; ?></p>
    </main>
</body>
</html>

The administrator message is rich text set by a trusted user, so it is printed as is, as the system template does. If members must log in while the site is offline, copy the login form of templates/system/offline.php; it posts to index.php with the user.login task, a return value and a form token created by HTMLHelper::_('form.token'). Remember that the offline page is served with status 503 so crawlers retry later.

Language files

Joomla language files are INI-style files named after the template with a tpl_ prefix. Two files per language are conventional:

  • tpl_tallpine_minimal.ini holds the strings used on the front end and on the template style edit form.

  • tpl_tallpine_minimal.sys.ini holds the strings the installer and the administrator lists need: the description and the position names.

; en-GB/tpl_tallpine_minimal.sys.ini
TPL_TALLPINE_MINIMAL="Tallpine Minimal"
TPL_TALLPINE_MINIMAL_XML_DESCRIPTION="A small, accessible standalone template for the Tallpine Hiking Club."
TPL_TALLPINE_MINIMAL_POSITION_HEADER="Header"
TPL_TALLPINE_MINIMAL_POSITION_MENU="Main menu"
TPL_TALLPINE_MINIMAL_POSITION_SIDEBAR="Sidebar"
TPL_TALLPINE_MINIMAL_POSITION_FOOTER="Footer"

; en-GB/tpl_tallpine_minimal.ini
TPL_TALLPINE_MINIMAL_FLUID_LABEL="Fluid container"
TPL_TALLPINE_MINIMAL_TITLE_LABEL="Site title"
TPL_TALLPINE_MINIMAL_SKIP_TO_CONTENT="Skip to main content"
TPL_TALLPINE_MINIMAL_MAIN_NAVIGATION="Main navigation"
TPL_TALLPINE_MINIMAL_SIDEBAR="Sidebar"
TPL_TALLPINE_MINIMAL_ERROR_HEADING="This trail does not exist"
TPL_TALLPINE_MINIMAL_BACK_HOME="Back to the home page"

Source: the Cassiopeia language files in the joomla-cms source tree

(The two files are shown in one listing; they are separate files on disk.) Rules that save debugging time:

  • Keys are uppercase, use underscores and must start with the template’s prefix so they cannot clash with other extensions. Values are in double quotes; a double quote inside a value is written "_QQ_".

  • The en-GB files must exist even if the site is German. They are the fallback for any language without a translation, and a missing key shows as the raw key.

  • Add de-DE files for the second language of the Tallpine site, and list them in the manifest. A template does not need to be translated to be installed, but untranslated keys appear as shouting capitals.

  • The same files may be loaded from language/ of the Joomla root (where language packs install) or from the template’s own folder. A site-wide override in System > Language Overrides wins over both; see Language files and internationalisation.

  • Use Text::_() for plain strings and Text::sprintf() for strings with placeholders. Do not concatenate sentences: word order differs between languages.

SCSS build options

The stylesheet above is hand-written CSS, which is enough for a small template. Larger templates usually write SCSS and compile it to CSS before packaging. Joomla itself compiles Cassiopeia from SCSS sources with a Node.js build in the source repository, and the template manifest lists a scss media folder so the sources can ship with the template. The decision is about the developer’s toolchain, not about Joomla: the web server serves only the compiled CSS in the css folder. Three practical options:

Option How When to choose it

Dart Sass command line

sass --no-source-map media/scss:media/css from a package.json script

A few files, no JavaScript bundling, the simplest setup

Vite (or another bundler)

A vite.config.js with build.rollupOptions.input for the SCSS and JS entry points, output to media/css and media/js

SCSS and JavaScript in one pipeline, hashed names, watch mode with fast rebuilds

No build step

Hand-written CSS with custom properties and logical properties

Small templates, and teams without Node.js

{
  "name": "tpl-tallpine-minimal",
  "version": "1.0.0",
  "private": true,
  "scripts": {
    "build:css": "sass --no-source-map --style=compressed media/scss:media/css",
    "watch:css": "sass --watch --style=expanded media/scss:media/css"
  },
  "devDependencies": {
    "sass": "1.89.2"
  }
}

Advice for all three options:

  • Use the sass npm package (Dart Sass). LibSass and Node Sass are deprecated and no longer receive features; Dart Sass is the only maintained implementation. Language details are in Sass.

  • Compile in CI and commit or package the compiled files. The Joomla site must never need Node.js, and the installer copies only the files named in templateDetails.xml. The compiled template.css and a template-rtl.css have to exist before packaging, or the page loads without styles and the log shows nothing.

  • Generate the RTL stylesheet from the same sources (for example by building with a small rtl partial that flips direction-dependent rules) or use logical properties and keep the stylesheet shared.

  • Keep colour tokens as CSS custom properties so the same stylesheet supports the dark colour scheme described in the Manual page "Color scheme support": the template sets the data-color-scheme attribute on <html> and fires the joomla:color-scheme-change event so extensions such as editors can follow it.

  • If you import Bootstrap’s SCSS, import only the parts you use. Joomla’s own scripts do not require Bootstrap CSS in a template, but the core layouts and the administrator-facing components emit Bootstrap 5 class names on the front end, so a template without Bootstrap must style those classes itself.

The pipeline is wired into the package build in Build and CI for extensions.

Accessibility checklist

Accessibility is partly a template’s responsibility, because the template owns the page skeleton, the colour and focus styles and the order of landmarks; components and modules own their own markup. The general principles, tools and WCAG success criteria are explained in Web accessibility and the Joomla-specific colour, contrast and right-to-left notes are in Colour modes, RTL and accessibility. For a Joomla template, check at least:

  • Document language and direction: <html lang="..." dir="..."> from $this->language and $this->direction, so screen readers pick the right voice in each language of the multilingual Tallpine site.

  • Landmarks: one <header>, one <main>, <nav> and <aside> with aria-label where there is more than one of a kind, and a <footer>. Render a landmark only if its position has modules.

  • Skip link: the first focusable element jumps to <main id="content">, is visible on focus and has enough contrast.

  • Headings: the template provides the site name as a link or <p>, not as an <h1>; the article or component supplies the single <h1>. Module chromes must not skip heading levels.

  • Focus: never outline: none without a replacement; use :focus-visible with a 3:1 contrast ring.

  • Contrast: text 4.5:1 (large text 3:1), non-text controls 3:1, in the light scheme and in the dark scheme if you offer one.

  • Zoom and reflow: the viewport meta tag does not set user-scalable=no or a maximum scale; the layout reflows at 320 CSS pixels without horizontal scrolling and works at 200% text size.

  • Motion: respect prefers-reduced-motion.

  • System messages: keep <jdoc:include type="message" /> before <main> so errors and notices are announced; the default message layout uses an alert role.

  • Images: the logo has a text alternative (or the brand name is also printed as text); decorative images use an empty alt.

  • Forms and links: do not remove the label styling that components emit; links must be distinguishable by more than colour.

  • Error and offline pages: they have a language, a <main>, a visible <h1> and a way back to the site.

  • Test: run an automated checker (axe, Lighthouse, WAVE), then tab through every layout with the keyboard and with a screen reader. Automated tools find only a part of the issues.

Joomla’s own guidance, the Joomla Accessibility Team’s work and the Manual are the places to look for accessibility conventions when the template needs to follow Cassiopeia’s markup closely.

Packaging and installing

The template is installed as an extension package: a zip file with templateDetails.xml at its root. The folder name inside the zip does not matter; the name element and the file lists do. Build the package from the template folder, excluding development files:

cd tpl_tallpine_minimal
npm ci && npm run build:css
zip -r ../tpl_tallpine_minimal-1.0.0.zip . \
  -x 'node_modules/*' -x 'package*.json' -x '*.map' -x '.DS_Store'

Then install it:

  1. Log in to the administrator and open System > Install > Extensions.

  2. Use Upload Package File, or Install from Folder if the unpacked folder is on the server, or Install from URL.

  3. The installer reads the manifest, copies the files to templates/tallpine_minimal, the media folder to media/templates/site/tallpine_minimal and the language files, then creates a default template style named after the template.

  4. Open System > Site Template Styles, make the new style the default (the star icon) or assign it to chosen menu items under Menu Assignment, then edit it to set the parameters.

  5. Open the front end, check the page in the browser’s network panel for 404s on CSS and JS, then use ?tp=1 to check the positions.

From the command line, the same install can be scripted with php cli/joomla.php extension:install --path=...; see Command-line interface.

For update delivery, add an <updateservers> element to the manifest pointing at an XML file on a server you control; update sites, download keys and the Joomla Extensions Directory are covered in Update servers and the JED and Extension types, manifests and packaging. If the template ships with the pkg_tallpine package, add it as a <file type="template" client="site" id="tallpine_minimal"> entry in the package manifest so one upload installs the component, module, plugins and template together.

Common errors and pitfalls

  • Installation fails with "Unable to find install package" or "XML setup file not found". templateDetails.xml is not at the zip root, or the zip contains one wrapping folder. Re-zip from inside the template folder.

  • The template installs but the page has no styles. The compiled CSS is missing from the zip, the uri in joomla.asset.json does not match a file in media/.../css, or joomla.asset.json is not listed in <files>.

  • Positions show raw keys. The .sys.ini file is missing from the manifest’s <languages> list.

  • Uninstalling or reinstalling resets styles. Removing a template deletes its template styles and the menu assignments that used them; menu items fall back to the default style. A default style cannot be deleted while it is the default.

  • Editing files in templates/ on the server. The change vanishes the next time the package is installed. Keep the template in version control and change the package.

  • Reusing cassiopeia as the name. A name that equals a core template is overwritten by core updates. Always choose a prefix of your own.

  • Leaving index.php without defined('_JEXEC') or die;. Without the guard the file can be executed directly from the web, which discloses paths in error messages. In Joomla 6 the guard is _JEXEC; JPATH_PLATFORM is no longer defined without the Backward Compatibility 6 plugin.

  • Using a template as a security boundary. A template controls presentation only; access control is applied by components and menu items. See Security in extensions.

Where to go next

The minimal template is deliberately not a replacement for Cassiopeia: it has no menu styling, no card chrome and no colour schemes. For most sites the better route is to start from a child template, as in Joomla child templates, and add overrides as in Template overrides and alternative layouts. Third-party frameworks and page builders that provide their own templates are discussed in Template frameworks and page builders.