Feature: Appearance switcher
Description
The theme now ships the script and the control that let a visitor change
appearance, palette, and the main menu - the piece
Feature: Navigation shipped without. Three new
TypoScript constants under theme., in
Configuration/, set the server-rendered
default; an inline script and Resources/
apply a visitor's stored choice and operate the controls from there.
| Constant | Values | Default |
|---|---|---|
theme. | auto, light, dark | auto |
theme. | neutral, ember, ocean,
moss, violet | neutral |
theme. | on, off | on |
These are constants, not Site Settings. A constant is read identically by the
site set and by the classic sys_ static include, which is the
property this theme is built around; a settings. would
only serve the set and introduce a second source of truth for the same value.
A site package that wants these editable in the Site Settings UI can still
declare them in its own set.
Server-rendered attributes
config. writes every value onto the
<html> tag raw - Request only
applies std when a matching . sub-array is configured, and
none is here - so a constant is the only thing that can go on this path, never
a cObject:
config.htmlTag.attributes.data-palette = {$theme.appearance.palette}
config.htmlTag.attributes.data-theme-content-outline = {$theme.appearance.contentOutline}
["{$theme.appearance.default}" != "auto"]
config.htmlTag.attributes.data-theme = {$theme.appearance.default}
[END]
data- is behind that condition rather than assigned
directly because auto means the absence of the attribute,
not a value for it - no selector matches data-, and
rendering it would look like a working default while doing nothing.
The no-flash script
An inline script in the document head applies a stored appearance and palette
before first paint. It is emitted through page.
rather than f:, because the asset collector may move
a script to the end of the body, and a stored dark appearance would then paint
light first - the exact flash this script exists to prevent.
It sets data- on the root first and unconditionally, then reads
local for the stored appearance and palette, each inside its own
try/catch. local throws rather than returning
null in Safari's private mode and when cookies are blocked, and an
uncaught throw would abort the script before the marker was set - which is
why the marker comes first, not after. Losing it costs more than a missed
colour choice: data- is also what
the navigation collapse and the switcher's
visibility are gated behind, so a page that lost it keeps the always-expanded
navigation and a permanently hidden switcher for the rest of that visit.
The switcher itself is hidden until data- is set, for the same
reason: a control that cannot work yet is a visible promise the page cannot
keep, not a degraded one.
The module script
Resources/, loaded with
page. and .type = module. A module
is deferred by specification, so it needs no defer attribute, and it
runs in the footer rather than the head because it only wires up listeners on
elements that already exist by then. It owns the appearance control, the
palette control, and - now that its script has arrived - the main menu
toggle that Feature: Navigation shipped inert.
The file uses no optional chaining (?.) anywhere. A browser that does
not recognise type="module" skips the element unparsed and can never
fail on syntax inside it, but "recognises modules" and "supports optional
chaining" are not the same floor - module support landed 2017-2018, optional
chaining in 2020 - and a syntax error anywhere in a module aborts the whole
file, with no per-statement fallback the way a classic script has. Plain
if checks cost nothing and remove that failure mode entirely.
Palette swatches
Each palette button in the switcher carries a small swatch in that palette's
primary colour. The swatch colour is the one duplicated colour in the
stylesheet: every palette lives entirely inside its own
[data- selector, and CSS has no mechanism to ask what a
custom property would resolve to under a different attribute value, so the
swatch cannot reference the token live. Component
is what keeps the copy from drifting out of step with
abstracts/_.
Impact
No cookie is set anywhere in this mechanism, and nothing is persisted server
side. local is the whole story - two keys,
theme- and theme- - which is also why there is no
consent question to design around: the choice never leaves the browser it was
made in.
The main menu toggle, shipped inert in
Feature: Navigation, is now fully wired: clicking
it flips aria-, Escape and an outside click close the
menu again. Nothing about the navigation's no-JavaScript layout changes -
the collapse remains gated behind data-, exactly as before.
Note
Changing appearance or palette never moves focus, and no live region announces the change - a visible palette change does not need one, and one would be noise on every click.