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.
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:
-
The default theme's
<link>element must carryid="wexa-theme". Without it,ThemeManagercreates a second link element and themes accumulate instead of replacing each other. -
The default theme must be registered with
ThemeManagerand declared viasetDefault().
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"
| Written | Registered |
|---|---|
| Nothing | wexa_theme, aurora, highcontrast, in that order. |
| A name alone | That theme of the framework, and it alone: naming one is choosing among them. |
name:path | The theme the page brings, and then the three of the framework — bringing one adds it, and says nothing of the others. |
| Both | What 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>