Joomla Colour Modes, RTL and Accessibility

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.

Three things decide whether a Joomla template works for everybody who opens it: how it behaves when the visitor prefers a dark appearance, how it behaves when the page language is written from right to left, and whether people using a keyboard, a screen reader or high magnification can use it. They are different mechanisms in the code, but they share one testing habit: a template is only finished when it has been checked in every combination of colour scheme, text direction and input method that its audience uses. This page explains the colour scheme API that Joomla defines and where it is implemented in 6.2, how Bootstrap 5.3 colour modes relate to it, how Joomla switches text direction and loads direction- specific CSS, how to use the accessibility checker that ships in the editor (Jooa11y), what the project commits to for accessibility, and how to test the Tallpine Hiking Club’s cassiopeia_tallpine child template for all of it.

It was checked against the official documentation and the 6.2 source tree on 2026-10-30. The behaviour described is that of Joomla 6.2 (current, requiring PHP 8.3 or newer; the tree reports 6.2.1). The colour scheme API and the Atum colour scheme setting arrived in Joomla 5.1 and the Jooa11y plugin in Joomla 4.1, so everything here also applies to Joomla 5.4, which receives security fixes only. Joomla 4.x and 3.x are end of life. The audience is the themer, the site builder and the content author; the developer sections need PHP only where a template index.php is overridden.

The general theory of accessibility (the W3C, the POUR principles, conformance levels, legal standards and tools) is in Web accessibility and is not repeated; this page covers what is specific to Joomla. How a child template is built is in Joomla child templates, the options of the default template are in Cassiopeia and Cassiopeia Extended, and the asset registry used below is in The Web Asset Manager.

Colour schemes: the Joomla API

Joomla calls the feature a colour scheme (users call it dark mode). The Developer Manual defines it as a small contract between a template and everything that renders inside it, such as editor plugins, rather than as a set of styles. The contract has two attributes on the <html> element and one event:

Item Meaning

data-color-scheme-os

The template follows the operating system setting and switches automatically through CSS media queries. The attribute is present, with no value, while the template is in "follow the system" mode.

data-color-scheme="light"

The template is currently showing the Light scheme, whether it was chosen automatically or by a custom switch.

data-color-scheme="dark"

The template is currently showing the Dark scheme, whether chosen automatically or by a switch.

joomla:color-scheme-change

A DOM event the template fires on document whenever the scheme changes, so that listeners can react: for example, an editor plugin loading a matching light or dark skin.

The purpose is stated by the Manual: setting data-color-scheme lets extensions load the matching theme. Core code and third-party JavaScript read the attribute, not the colours, so a template that implements dark mode with its own class names but never sets the attribute leaves the TinyMCE editor in frontend editing, the Jooa11y panel and any extension that listens to the event in light mode on a dark page. The Manual’s example of announcing a change is two lines:

document.documentElement.dataset.colorScheme = 'dark';
document.dispatchEvent(new CustomEvent('joomla:color-scheme-change', { bubbles: true }));

The Manual page is short and carries no CSS: it defines the signals, not the styling. The colours themselves are the template’s business. The API belongs to the 5.x series (the change log of 5.1.0 lists "Dark Mode final Version" and "Template color scheme configuration" among its pull requests), which is the reason the older books do not describe it: Marzo’s Joomla! 4 Masterclass (2023) targets Joomla 4 and Cámara Mora’s Developing Extensions for Joomla! 5 (2023) targets 5.0, so neither knows data-color-scheme, the joomla:color-scheme-change event or the Atum setting below.

Atum implements it, Cassiopeia does not

In Joomla 6.2 the administrator template Atum implements the API completely, and the site template Cassiopeia does not.

Atum’s templateDetails.xml has a colorScheme list parameter with four options (Disabled, Follow the system, Light and Dark) whose default is os, and a link-color-dark colour field for the link colour used in the dark scheme. In Atum’s index.php the scheme is resolved in a fixed order, and the result is written into the <html> tag together with dir and lang:

flowchart TD S[Request for an administrator page] --> C{Cookie userColorScheme<br/>set to light, dark or os?} C -- yes --> A[Use the cookie value] C -- no --> U[User parameter colorScheme,<br/>else the template parameter] U --> O{Value is os?} O -- yes --> K[Read cookie osColorScheme<br/>written by the page script] O -- no --> A K --> A A --> H["html gets data-color-scheme-os<br/>or data-bs-theme and data-color-scheme"] H --> E[Script fires joomla:color-scheme-change<br/>when the scheme changes]

In words: a user’s explicit choice (the userColorScheme cookie, set by the colour scheme toggle in the administrator header) wins; otherwise the account parameter colorScheme is used, falling back to the template style’s setting; when the value is os, a second cookie, osColorScheme, records what the operating system currently says so that the server can render the right attributes on the first byte and avoid a flash of the wrong scheme. The mapping Atum writes is:

Value Attributes added to <html>

os

data-color-scheme-os, plus the attributes for the detected OS scheme (from the osColorScheme cookie)

light

data-bs-theme="light" data-color-scheme="light"

dark

data-bs-theme="dark" data-color-scheme="dark"

Two details matter to administrators. First, the dark scheme replaces only what Atum’s own SCSS maps to data-color-scheme (for example the link colour is overridden in an inline :root[data-color-scheme="dark"] rule); the Atum colour fields in System → Administrator Template Styles describe the light scheme. Second, a user can choose a scheme in their own account, and that preference overrides the template style, so an administrator who sets the template style to Light has not forced the scheme for people who picked Dark or Follow the system for themselves.

Cassiopeia’s index.php writes lang and dir on <html> but no colour scheme attribute, and its templateDetails.xml has no colour scheme parameter. The shared SCSS variables compile Bootstrap with $enable-dark-mode: false, which means the stylesheet contains none of Bootstrap’s dark variables; the maintainers' comment next to it records that dark mode was kept out of the 4.x series to avoid backward-compatibility breaks and is planned for 7.0. The conclusion for the Tallpine site is simple: in 6.2 a member whose phone is set to dark sees the light Cassiopeia site, and cassiopeia_extended, the core child template added in 6.0, changes colours and font sizes only. The state of the template and the three options for a dark site are summarised in Cassiopeia and Cassiopeia Extended; the rest of this section builds the first of them.

Bootstrap 5.3 colour modes and how they relate

Joomla 6.2 bundles Bootstrap 5.3 (the source tree depends on ^5.3.8). Bootstrap added colour modes in 5.3.0: the data-bs-theme attribute, set on <html> for the whole page or on a single component, selects a light or a dark set of CSS custom properties (--bs-body-bg, --bs-body-color, --bs-link-color, --bs-border-color and so on). Bootstrap has no built-in picker. Its documentation uses a small script that reads a saved value from localStorage, falls back to prefers-color-scheme and writes the attribute. The Sass switch $enable-dark-mode turns the dark variables on or off at compile time, and $color-mode-type: media-query makes the same rules follow the operating system without an attribute.

Joomla’s two mechanisms are therefore complementary and not interchangeable:

  • data-bs-theme is Bootstrap’s: it restyles Bootstrap components (alerts, cards, form controls, dropdowns) through variables. It only does something if the CSS was compiled with dark mode enabled. Atum was, Cassiopeia was not.

  • data-color-scheme is Joomla’s: it is the signal that extensions read. Atum writes both attributes together, which is the pattern a site template should follow.

  • Bootstrap SCSS variables such as $enable-dark-mode are fixed at compile time, so a child template cannot flip them at runtime. A child can ship its own compiled CSS, or set the runtime custom properties itself.

For cassiopeia_tallpine the practical route is the second one: write the dark values as custom properties in user.css, select them with the Joomla attribute and with a media query, and set the CSS color-scheme property so that the browser draws scrollbars, form controls and the default canvas to match.

A dark scheme for cassiopeia_tallpine

The following stylesheet is loaded by the child template. It defines a dark palette once, applies it when the visitor chooses Dark, and also when the visitor chose nothing and the system is dark. The palette has been picked so that body text on the background is well above the 4.5:1 contrast ratio that WCAG asks of normal text; always re-measure when you change a colour.

/* media/templates/site/cassiopeia_tallpine/css/user.css */
:root {
  color-scheme: light;
  --tallpine-bg: #ffffff;
  --tallpine-text: #1d2a22;
  --tallpine-link: #14532d;
  --tallpine-surface: #f1f5f2;
}

/* Visitor (or script) chose Dark explicitly. */
:root[data-color-scheme="dark"] {
  color-scheme: dark;
  --tallpine-bg: #101a14;
  --tallpine-text: #e6ede8;
  --tallpine-link: #8fd3a8;
  --tallpine-surface: #1b2a21;
}

/* No explicit choice: follow the system. */
@media (prefers-color-scheme: dark) {
  :root:not([data-color-scheme]) {
    color-scheme: dark;
    --tallpine-bg: #101a14;
    --tallpine-text: #e6ede8;
    --tallpine-link: #8fd3a8;
    --tallpine-surface: #1b2a21;
  }
}

body {
  background-color: var(--tallpine-bg);
  color: var(--tallpine-text);
}

a:not(.btn) {
  color: var(--tallpine-link);
}

.card,
.mod-banners {
  background-color: var(--tallpine-surface);
  color: var(--tallpine-text);
}

/* Visible focus in both schemes. */
:focus-visible {
  outline: 3px solid currentcolor;
  outline-offset: 2px;
}

The duplication of the palette (once for the attribute, once for the media query) is deliberate: CSS cannot share a rule between an attribute selector and a media query. In a build with Sass or PostCSS it becomes a mixin; with plain user.css it stays as written. The selector :root:not([data-color-scheme]) matters: without it the media query would override a visitor’s explicit Light choice on a dark system.

The stylesheet above only responds to a scheme; a switch is what sets the attribute. The script below stores the choice, applies it before first paint, and fires the Joomla event. It is registered as an asset in the child template’s joomla.asset.json and loaded from the template’s index.php.

// media/templates/site/cassiopeia_tallpine/js/colour-scheme.js
(() => {
  const KEY = 'tallpineColourScheme'; // 'light' | 'dark' | absent = follow system
  const root = document.documentElement;

  const read = () => {
    try { return localStorage.getItem(KEY); } catch (e) { return null; }
  };

  const apply = (scheme) => {
    if (scheme === 'light' || scheme === 'dark') {
      root.dataset.colorScheme = scheme;
      root.dataset.bsTheme = scheme;
    } else {
      delete root.dataset.colorScheme;
      delete root.dataset.bsTheme;
    }
    document.dispatchEvent(new CustomEvent('joomla:color-scheme-change', { bubbles: true }));
  };

  apply(read());

  document.querySelectorAll('[data-tallpine-scheme]').forEach((button) => {
    button.addEventListener('click', () => {
      const scheme = button.dataset.tallpineScheme; // light, dark or auto
      try {
        if (scheme === 'auto') { localStorage.removeItem(KEY); } else { localStorage.setItem(KEY, scheme); }
      } catch (e) { /* storage blocked: the choice lasts for this page only */ }
      apply(scheme);
      document.querySelectorAll('[data-tallpine-scheme]').forEach((b) => {
        b.setAttribute('aria-pressed', String(b === button));
      });
    });
  });
})();
{
  "$schema": "https://developer.joomla.org/schemas/json-schema/web_assets.json",
  "name": "cassiopeia_tallpine",
  "version": "1.0.0",
  "description": "Assets of the Tallpine Hiking Club child template.",
  "license": "GPL-2.0-or-later",
  "assets": [
    {
      "name": "template.tallpine.colour-scheme",
      "description": "Light, dark and automatic colour scheme switch.",
      "type": "script",
      "uri": "js/colour-scheme.js",
      "attributes": { "defer": true },
      "dependencies": ["template.active"]
    }
  ]
}
<?php
// templates/cassiopeia_tallpine/index.php (excerpt; the rest is copied from the parent's index.php)

\defined('_JEXEC') or die;

/** @var \Joomla\CMS\Document\HtmlDocument $this */
$wa = $this->getWebAssetManager();
$wa->useScript('template.tallpine.colour-scheme');

Three pitfalls apply. First, the script is deferred, so on a slow connection the page paints once in the system scheme before the attribute is set; for no flash at all, inline the first line of the script in the head with addInlineScript(), at the price of an inline script under a strict Content Security Policy (see Security hardening, MFA and passkeys). Second, the template must keep a visible control that is a real <button> with aria-pressed, not a clickable icon, or keyboard users cannot reach the feature. Third, images with transparent backgrounds and text baked into images (the club logo, trail maps) do not follow the scheme: provide a light and a dark version or use SVG with currentcolor.

Right-to-left languages and the dir attribute

Arabic, Hebrew, Persian and Urdu are written from right to left, and a page in such a language needs a mirrored layout: the main column moves to the right, icons that imply direction (arrows, back and next) flip, and text is aligned to the right. Joomla supports this in both default templates: the Manual states that Cassiopeia and Atum support RTL out of the box.

How Joomla decides the direction

Every installed language declares its direction in its language metadata file (langmetadata.xml) through the <rtl> element: en-GB has <rtl>0</rtl>, and an Arabic pack has 1. When the page is built, the application sets the direction of the document from the active language, and the document exposes it to the template as $this->direction (a property that defaults to ltr and is changed with setDirection()). The template then does three things, all visible in Cassiopeia’s index.php:

  1. It writes the direction on the root element, <html lang="..." dir="rtl">. The dir attribute is what browsers use for text alignment, table column order, form control placement and the Unicode bidirectional algorithm; no stylesheet can replace it.

  2. It selects the right stylesheet. Cassiopeia’s joomla.asset.json has two assets, template.cassiopeia.ltr (template.min.css) and template.cassiopeia.rtl (template-rtl.min.css), and index.php picks one with $this->direction === 'rtl', then registers template.active so that the user and language stylesheets depend on the chosen one.

  3. It adds the class rtl to <body> ($this->direction == 'rtl'), a hook for rules that cannot be expressed with the attribute.

A child template inherits all of this: it needs no RTL work unless it adds its own CSS. A child template that copies index.php must keep the three lines above, or the RTL pack silently renders left to right.

Writing CSS that works in both directions

The Manual gives two techniques. The first is logical properties, which describe a side relative to the text flow, so that one rule serves both directions. The second is RTLCSS, a PostCSS plugin that generates an RTL stylesheet from the LTR one (Joomla’s own build uses rtlcss 4.3 to produce template-rtl.min.css). Logical properties are preferred because they need no second file; RTLCSS is for old code and for shorthand properties that logical properties do not cover.

/* Before: breaks in RTL, the badge stays on the left of the title. */
.trail-badge {
  margin-left: 0.5rem;
  padding-left: 0.75rem;
  border-left: 3px solid var(--tallpine-link);
  text-align: left;
}

/* After: one rule for both directions. */
.trail-badge {
  margin-inline-start: 0.5rem;
  padding-inline-start: 0.75rem;
  border-inline-start: 3px solid var(--tallpine-link);
  text-align: start;
}

Where a separate file is unavoidable, the asset template.active.language loads a stylesheet named after the active language, language/<tag>/<tag>.css relative to the template’s media folder, for example media/templates/site/cassiopeia_tallpine/language/de-DE/de-DE.css or .../ar-AA/ar-AA.css. That is the place for a different font stack or a longer-word fix (German labels are about a third longer than English ones) without touching the main stylesheet. Because the asset is a dependency of template.user, it is loaded before user.css.

The RTLCSS route is a one-off command for an LTR stylesheet that cannot be rewritten:

npm install --save-dev rtlcss@4.3.0
npx rtlcss media/templates/site/cassiopeia_tallpine/css/tallpine-extra.css \
  media/templates/site/cassiopeia_tallpine/css/tallpine-extra-rtl.css

The dir attribute inside content

The page direction is not the direction of every piece of text on it. Tallpine members write trail reports in German and English, and a future Arabic section would put Latin trail names (Zugspitze) inside right-to-left paragraphs. Without help, punctuation next to the foreign word lands on the wrong side. Two HTML tools fix that and belong in overrides and module layouts, not in the stylesheet:

  • dir="auto" on an element that holds user-supplied text (a comment, a username, a custom field value) lets the browser decide from the first strong character.

  • <bdi> isolates an inline fragment, such as a name in a list, so it cannot disturb the text around it.

<?php
// templates/cassiopeia_tallpine/html/mod_tallpine_nexthikes/default.php (excerpt)

\defined('_JEXEC') or die;

use Joomla\CMS\Language\Text;
?>
<ul class="mod-tallpine-nexthikes list-unstyled">
    <?php foreach ($list as $hike) : ?>
        <li>
            <bdi><?php echo htmlspecialchars($hike->title, ENT_QUOTES, 'UTF-8'); ?></bdi>
            <span class="text-muted">
                <?php echo Text::sprintf('MOD_TALLPINE_NEXTHIKES_DISTANCE', (float) $hike->distance); ?>
            </span>
        </li>
    <?php endforeach; ?>
</ul>

Common mistakes are hard-coded left and right in CSS and in inline styles (float: left, text-align: right), Bootstrap spacing classes written with the old names instead of the logical ms-*, me-*, ps-* and pe-* (Bootstrap 5 mirrors those automatically), directional icon fonts used without the mirrored variant, and a lang attribute on <html> that does not match the content. The lang attribute is also what screen readers use to choose the pronunciation, so a German article in an English-labelled page needs lang="de" on its container; see Multilingual sites for how the Language Filter and associations decide the language of a page.

The accessibility checker in the editor: Jooa11y

Joomla ships an author-side checker, the core system plugin System - Joomla Accessibility Checker (extension name plg_system_jooa11y, namespace Joomla\Plugin\System\Jooa11y, available since Joomla 4.1). Jooa11y is the Joomla build of Sa11y, itself a fork of Tota11y, and is meant for content authors: it highlights problems that an author can fix by editing content, and it is explicitly "not a comprehensive code analysis tool". Its checks are aligned with WCAG 2.1 level A and AA, and a few go beyond WCAG (the long-heading warning, for example).

Using it

  • Enable the plugin in System → Plugins (search for "Accessibility"). In the fresh 6.2 package the plugin exists; whether it is enabled depends on the installation, so confirm it in the plugin list.

  • In Content → Articles, open an article. The Articles: Edit toolbar has an Accessibility Check button that opens the checker for that article in a popup window, showing the rendered page of the article.

  • Anywhere on the site frontend, the checker is loaded when the URL has the query parameter jooa11y=1 or when the plugin option Show Always is on. Then a round icon appears at the bottom right of the page, showing the number of issues; the panel also toggles with Alt + A.

  • The panel lists errors (missing, placeholder or file-name alternative text; empty or generic link text such as "click here"; icon-only links with no name; table header problems; heading level problems; weak colour contrast; form inputs without labels; an undeclared page language) and warnings that need a human judgement (headings made with bold text, very long headings, redundant words in alternative text, all capitals, links that open in a new window without notice, PDF links, missing captions or transcripts). A green tick or a yellow cross can be selected to inspect the element.

  • The Show Settings tab switches on the optional checks: contrast, form labels, advanced link checks, a readability estimate and the checker’s own dark mode, and a colour filter that simulates four kinds of colour vision deficiency. The readability score is a Flesch reading-ease estimate from paragraph and list text (60 to 100 is considered good); it is a reference value only and does not affect the pass or fail count, and it works for English content only.

The checker identifies problems and never fixes them, and an empty report is not proof of conformance. It scans the page as rendered in the browser, so it cannot judge content it is told to skip, keyboard behaviour or the meaning of an alternative text.

Plugin options and how it loads

The plugin form has these settings, all in the Basic tab:

Option (parameter) Meaning

Show Always (showAlways)

Off (default) or On. On loads the checker on every page of the site for every visitor. Intended for development; leave it Off on a live site, or visitors see the panel.

Check Root (checkRoot)

CSS selector of the area to scan. Default main, so header, menus and footer are ignored.

Readability Root (readabilityRoot)

CSS selector of the area used for the readability score. Default main.

Container Ignore (containerIgnore)

Comma-separated CSS selectors that the checker skips, for third-party widgets the club cannot change.

Extra Props (extraProps)

A subform of key and value pairs passed to Sa11y as extra options. Numeric and boolean values are decoded from JSON; anything else stays a string.

Checks (checks)

A subform of key and value pairs that switch individual Sa11y checks on or off.

Internally the plugin is a final class extending CMSPlugin and implementing SubscriberInterface. On the site client only, it subscribes to onAfterRoute at high priority. If the request has jooa11y or showAlways is on, and the current user passes an optional group filter, it does two things: it vetoes the page cache for that request with an onPageCacheSetCaching listener (the report must never be cached for other visitors), and it registers a listener on onBeforeCompileHead. That listener loads the plugin’s language file, picks the Sa11y language from the active language tag (about 29 are available; any other falls back to English, and pt-BR, pt-PT, en-US and uk are mapped to their own files), passes the options to the page through addScriptOptions('jooa11yOptions', ...), and uses the Web Asset Manager to load the sa11y style and script, the language module and plg_system_jooa11y.jooa11y. Because the page is checked as built by the active template, a template that places the main content inside <main> (as Cassiopeia does) works with the default checkRoot; a template without a main landmark must change the option.

A site that runs the checker on the club’s staging server can set it up from the checker’s own settings, for example:

Check Root:        main
Readability Root:  main .com-content-article__body
Container Ignore:  .tallpine-map, .mod-banners
Show Always:       Off   (open any page with ?jooa11y=1 instead)

Pitfalls: the checker needs JavaScript and loads its code from the site, so a Content Security Policy that blocks the import map or inline script options stops it silently; and Show Always on a production site exposes the panel to every visitor and disables the page cache for every request.

The accessibility settings of the administrator

The administrator template has its own switches for people who need them, kept per user in the account’s Accessibility Settings tab. The Atum index.php reads four parameters from the logged-in identity and turns them into CSS classes: a11y_mono (monochrome, class monochrome on <body>), a11y_contrast (class a11y_contrast), a11y_highlight (class a11y_highlight, which emphasises links and focus) and a11y_font (class a11y_font on <html>, which switches to a more readable font). The template style also has a Monochrome switch that applies to everybody. These are user preferences and are separate from the colour scheme: a user may use dark scheme with high contrast. Because the settings are stored on the account, a new editor of the Tallpine site finds them in the account edit screen, not in Global Configuration.

The project’s WCAG commitments

Joomla’s accessibility statement says that the core and the administrator are built to give an accessible foundation: Joomla 4 and later conform to WCAG 2.1 (the W3C Web Content Accessibility Guidelines) and ATAG 2.0 (the Authoring Tool Accessibility Guidelines, which applies to the administrator and the editor as tools that create content). The statement positions Joomla as a base from which builders can produce sites at WCAG 2.1 level AA, and it places responsibility for the final result on the site builder. It also says that extensions are outside the project’s compliance responsibility and that the joomla.org websites themselves did not yet meet the guidelines when it was last updated (16 December 2022), with level AA as their target. Issues are reported through the Joomla Issue Tracker or GitHub, using the accessibility issue template.

What follows from that for the Tallpine site:

  • The default templates give a good start (a labelled back-to-top anchor, landmarks such as <header>, <main> and navigation modules, a lang and dir on <html>), but conformance is the result of choices: colours, logo alternative text, the markup of overrides, the content.

  • A third-party template, module or component must be checked on its own. The statement offers no guarantee for it.

  • The WCAG 2.1 AA success criteria that templates most often fail are 1.4.3 (text contrast 4.5:1, and 3:1 for large text), 1.4.11 (non-text contrast of 3:1 for icons, borders and focus), 1.4.10 (reflow at 320 CSS pixels), 2.4.7 and 2.4.11 (focus visible), 1.3.1 (info and relationships: headings, lists, tables) and 4.1.2 (name, role, value for custom controls such as the colour scheme switch). WCAG 2.2 (2023) adds criteria, among them focus not obscured; a sticky header on Cassiopeia can cover the focused element, so test with the header on. The wider conformance model is in Web accessibility.

  • Contrast must hold in each colour scheme separately: a green link that passes on white fails on the dark background unless it is lightened, as the palette above does.

Testing a template

A template is tested at four levels, from the cheapest to the most expensive; skipping the last one is the usual reason accessible-looking sites fail real users.

  1. Author checker (Jooa11y). Open the most important pages with ?jooa11y=1 after every template change: a list page, an article with custom fields, the member-only trail conditions page. Fix errors; read warnings.

  2. Automated rule engine. Run axe-core in the existing end-to-end suite. The pattern, with fixtures and ways to scope a scan, is in Accessibility checks in end-to-end tests; the Joomla-specific part is to run each page in each combination of scheme and direction.

  3. Manual keyboard and screen reader pass. Tab through the page, use the page zoomed to 200 percent and at 320 pixels wide, and try a screen reader (NVDA with Firefox, VoiceOver with Safari). Check the skip link, focus order, the switch button, and the focus indicator in both schemes.

  4. Visual comparison of light, dark and RTL variants for layout breakage, using the techniques of Visual regression testing.

A Playwright test that checks the Tallpine home page and a trail article in light, dark and (with a language pack installed) right-to-left, against the local Docker site of Local development with Docker:

{
  "name": "tallpine-a11y-tests",
  "private": true,
  "scripts": { "test:a11y": "playwright test" },
  "devDependencies": {
    "@playwright/test": "1.64.1",
    "@axe-core/playwright": "4.14.0"
  }
}
// tests/a11y.spec.js
import { test, expect } from '@playwright/test';
import AxeBuilder from '@axe-core/playwright';

const BASE = process.env.TALLPINE_URL ?? 'http://localhost:8080';
const pages = ['/', '/trails/alpine/zugspitze-ridge-walk'];

for (const scheme of ['light', 'dark']) {
  for (const path of pages) {
    test(`no WCAG 2.1 AA violations at ${path} in ${scheme}`, async ({ page }) => {
      await page.emulateMedia({ colorScheme: scheme });
      await page.goto(BASE + path);
      // Make the Joomla signal match the emulated system scheme.
      await page.evaluate((s) => { document.documentElement.dataset.colorScheme = s; }, scheme);

      const results = await new AxeBuilder({ page })
        .withTags(['wcag2a', 'wcag2aa', 'wcag21a', 'wcag21aa'])
        .exclude('.tallpine-map') // third-party map widget, checked separately
        .analyze();

      expect(results.violations).toEqual([]);
    });
  }
}

test('right-to-left direction reaches the document', async ({ page }) => {
  await page.goto(BASE + '/ar/');           // requires an Arabic language pack and content language
  await expect(page.locator('html')).toHaveAttribute('dir', 'rtl');
  await expect(page.locator('body')).toHaveClass(/\brtl\b/);
});

Automated engines find a minority of the problems. They cannot decide whether alternative text is meaningful, whether the reading order is logical, or whether a dark theme remains legible on a phone in sunlight. Treat the first two levels as regression guards and the third as the real check. Report core problems to the project, as the statement describes.

Common errors and pitfalls

  • Dark mode on Cassiopeia in 6.2. Setting data-bs-theme="dark" on a Cassiopeia page changes nothing in Bootstrap components, because dark variables were not compiled in. Write your own custom properties, as above.

  • Only the attribute or only the media query. A template that implements the media query but never sets data-color-scheme leaves editor skins and extensions in light mode; a template that sets only the attribute ignores the system preference.

  • Forgetting color-scheme. Without color-scheme: dark, scrollbars, native selects and date pickers stay light on a dark page.

  • Flash of the wrong scheme. A deferred script runs after the first paint. Resolve the scheme as early as possible, or accept the flash and say so.

  • Hard-coded left and right. Breaks every RTL language; use logical properties.

  • Copied index.php without direction logic. The RTL stylesheet is never loaded, because the choice between template.cassiopeia.ltr and template.cassiopeia.rtl lives in index.php.

  • Contrast checked in one scheme. Re-check every colour pair in each scheme, and the focus indicator on every background.

  • Show Always left on. Disables the page cache and shows the checker to everyone.

  • Relying on a green checker. No automated tool, Jooa11y included, replaces keyboard and screen reader testing.

Version notes and corrections of older material

  • 4.1 introduced Jooa11y (plg_system_jooa11y) and child templates.

  • 5.1 introduced the colour scheme API, Atum’s dark scheme and its colour scheme setting, and updated Jooa11y to a newer Sa11y build. The Developer Manual page documents the API for 6.1 (Current).

  • 6.0 shipped cassiopeia_extended; the 6.2 SCSS still compiles Cassiopeia without dark variables, with dark mode planned for 7.0, and the Cassiopeia-prefixed link colour is deprecated (read --link-color).

  • Older books such as Marzo’s Joomla! 4 Masterclass (2023) and Cámara Mora’s Developing Extensions for Joomla! 5 (2023) predate the colour scheme API and describe Atum’s appearance without a dark mode; tutorials of the same period show jQuery or Bootstrap 4 patterns that no longer apply. Since 5.1 a dark administrator is a setting, not a custom stylesheet.

  • The wiki pages on docs.joomla.org about RTL and accessibility are legacy material; the Developer Manual and the User Guide are the current sources.