Whakerexa > Getting started > Themes

Themes

What is a theme?

A theme is a standalone CSS file. It redefines CSS variables and decoration rules — colors, gradients, borders, typography accents — without touching structure or accessibility layers.

Themes are mutually exclusive: each theme replaces the previous one entirely. They never stack. This is enforced by ThemeManager, which manages a single <link id="wexa-theme"> element whose href is swapped on every theme switch.

Unlayered colors, layered accents

Most color variables (--bg-color, --border-color, --nav-bg-color, --buttons-bg-color…) are declared unlayered in a theme file — no @layer wrapper. An unlayered rule always wins over any @layer rule, regardless of selector specificity or load order, so this guarantees the active theme's colors beat wexa.css's own @layer theme defaults every time.

--custom-color1 and --custom-color2 (the accent pair used for chapter titles in book.css and slide headings in slides.css) are the exception: they are declared inside @layer theme. This lets a single document write its own unlayered override for a specific element — see wexa_theme_highcontrast.css's flat .slide h1 border — without a specificity fight against the theme file.

What this is for, what it deliberately does not do, and why: themes-design.html.

Integration

Two requirements make theme switching work correctly:

  1. The default theme's <link> element must carry id="wexa-theme". Without it, ThemeManager creates a second link element and themes accumulate instead of replacing each other.
  2. The default theme must be registered with ThemeManager and declared via setDefault().

Never load a theme file with a static <link> in addition to registering it. ThemeManager only ever swaps the href of #wexa-theme — it does not remove a second, separately linked copy. If that file is also needed for page-specific rules that must always be present (layout, custom classes for one document), split it in two: the page-specific file stays statically linked, and only the color/variable file is registered as a theme.

Add id="wexa-theme" to the default theme link in <head>.

<link id="wexa-theme" rel="stylesheet"
      href="wexa_statics/css/themes/wexa_theme.css" />

The active theme name is persisted in the URL parameter wexa_theme and propagated to all internal links via setUrlWithParameters().

const themes = new ThemeManager();
themes.register('wexa_theme',   'wexa_statics/css/themes/wexa_theme.css');
themes.register('aurora',       'wexa_statics/css/themes/wexa_theme_aurora.css');
themes.register('highcontrast', 'wexa_statics/css/themes/wexa_theme_highcontrast.css');
themes.setDefault('wexa_theme');
window.themes = themes;

A page says on the tag of the loader which themes it takes, and in which order it cycles through them. A name alone is one of the framework, which it names without saying where it stands. A name:path is a theme the page brings, its path read from data-base only when it is written bare: one written ./ or ../ is read from the page.

They are declared there and not after the page is loaded: the manager reads the address as soon as it is built, and a theme registered afterwards is a name it has already refused.

<!-- All three of the framework: say nothing. -->
<script src="wexa_statics/js/wexa.loader.js"
        data-base="wexa_statics/"></script>

<!-- Some of them, in the order of the cycle. -->
<script src="wexa_statics/js/wexa.loader.js"
        data-base="wexa_statics/"
        data-themes="wexa_theme, highcontrast"></script>

<!-- One of them, and nothing else: the button then has nothing to cycle. -->
data-themes="highcontrast"

<!-- One the page brings, then the three of the framework. -->
data-themes="swapp:./statics/swapp_theme.css"

<!-- One the page brings, and two of the framework, and nothing else. -->
data-themes="swapp:./statics/swapp_theme.css, aurora, highcontrast"
What a page writes, and what it gets
WrittenRegistered
Nothingwexa_theme, aurora, highcontrast, in that order.
A name aloneThat theme of the framework, and it alone: naming one is choosing among them.
name:pathThe theme the page brings, and then the three of the framework — bringing one adds it, and says nothing of the others.
BothWhat is written, in the order it is written, and nothing else: the framework's are named, so they are chosen.

The names the framework answers to are wexa_theme, aurora and highcontrast.

data-default names the one applied when the address carries no wexa_theme parameter. It has to be one of those the page takes.

A page that wants its own theme and none of the framework's does not have to say so: it shows no switching button. A registered theme costs nothing until it is activated — no stylesheet is asked for — and nothing activates it. The one exception is a reader who writes ?wexa_theme=aurora in the address himself.

Available themes

wexa_theme

Default theme. Navy and teal palette, animated gradient underline on links, subtle background gradients.

aurora

Cool blues and greens. Inspired by northern lights. Soft glow on interactive elements.

highcontrast

OS-style high contrast. White/black with dark-blue or yellow links (WCAG AAA). No gradients, no border-radius, flat colors throughout. Designed for maximum legibility.

Creating a custom theme

Redefine only the variables you need — all others fall back to wexa.css defaults. Keep colors unlayered; put --custom-color1/--custom-color2 inside @layer theme, matching the built-in themes.

Write it beside your page, then declare it on the tag of the loader, under the name it will answer to: data-themes="mytheme:./css/my_theme.css", and data-default="mytheme" if it is the one to apply when the address says nothing. Add the names of the themes of the framework to keep them in the cycle.

A group of variables is answered whole, in the two modes: a ground without the text laid on it borrows a colour from another palette. See CSS variables for the groups. The .contrast class belongs to AccessibilityManager — a theme must not override colours in contrast mode.

/* my_theme.css */
:root:not(.dark) {
    --bg-color: rgb(255, 250, 240);
    --a-color:  rgb(180, 60, 0);
}

.dark {
    --bg-color: rgb(20, 15, 10);
    --a-color:  rgb(255, 160, 80);
}

@layer theme {

    :root:not(.dark) {
        --custom-color1: rgb(80, 20, 0);
        --custom-color2: rgb(180, 60, 0);
    }

    .dark {
        --custom-color1: rgb(255, 220, 180);
        --custom-color2: rgb(255, 160, 80);
    }

}
<script src="wexa_statics/js/wexa.loader.js"
        data-base="wexa_statics/"
        data-themes="mytheme:./css/my_theme.css, wexa_theme, highcontrast"
        data-default="mytheme"></script>