Light and dark

A visitor picks light or dark once, in the user button, and expects the whole thing to follow: Meerkat's own pages and the application behind them. The pages are ours to paint. The application is yours, and no two of them read a scheme the same way - which is why the mechanism is declared on the route rather than guessed.

This page is written for the person integrating an application. For the pages Meerkat serves itself, see the built-in pages.

Where the choice lives

The visitor's choice is a cookie, MEERKAT_SCHEME, holding light, dark or auto, kept for a year and SameSite=Lax. It is also stored on their account, so it is put back on a browser that has never seen them.

auto means "follow the system", and it is the default: nothing is imposed until somebody chooses.

What the gateway always does

On a UI route whose scheme is not none, the gateway injects a small agent. When a scheme is chosen, it sets on <html>:

On auto both are removed, and the browser is back to following the system.

That alone is enough for an application whose CSS is written on prefers-color-scheme or on light-dark(). Everything below is for applications that switch some other way.

Declaring the mechanism

Four answers, and one of them is "there is nothing to switch".

Declaring the colour scheme on a route

Mechanism What the gateway writes Typical markup
(none chosen) the CSS color-scheme only your CSS reads prefers-color-scheme
attribute one attribute, named by you, always written, to the light value or the dark one <html data-theme="dark">
add-attribute the two values are attribute names, added bare and removed like classes <body dark-theme>
class the two values are class names, the two removed and the current one added <body class="dark">
none nothing at all light and dark mean nothing to this UI

Two more fields shape it:

Which is why an empty value means something in the last two: nothing on the tag in that state. That is the most widespread shape there is - nothing in light, dark in dark:

Mechanism light dark Result
class (empty) dark <body> in light, <body class="dark"> in dark
add-attribute (empty) dark-theme <body> in light, <body dark-theme> in dark
attribute on data-theme light dark always written, one or the other

The tag may not be parsed yet - the agent runs from the head, on purpose, so <html> is dressed before the first paint. A <body> that does not exist yet is caught up with once the document is parsed, and never written on <html> instead: a class left on the wrong element is a theme nothing removes.

What "none" is, and what it is not

none says this UI has no colour scheme of its own. It is not "takes the CSS color-scheme and nothing more" - that is leaving the mechanism unset. It is: light and dark mean nothing here.

The switch is then not offered in the user button, and the agent leaves the document alone.

Offering the switch, and dressing the button

Two things that used to travel together and are now separate:

When a route does not offer the switch, it says instead what the injected button itself wears: light, dark, or the visitor's own choice. That exists for the application with one look and no switch, where the button otherwise follows the visitor's system and floats light on a dark page. It dresses the chrome alone; the page is never touched.

Applications that keep their own theme

Here is the case that bites, and the reason this page exists.

An application that remembers its own light or dark in localStorage restores it on load, over whatever the gateway just applied. The visitor's choice holds until the app's own script runs, then snaps back. The same portal then looks right in one browser and wrong in another, and what differs is only what that browser had in store.

Fighting it on the document does not work. The way to work with it is to speak its own storage.

Field What it is
Override the application's stored theme the switch. Without it the gateway never touches the application's storage
Key the localStorage key the application keeps its choice under: theme, color-mode, vuetify:theme...
Light what to write for light. Defaults to light
Dark what to write for dark. Defaults to dark
System what "follow the system" is called there. Empty removes the entry instead

The values are the application's own vocabulary - dark, night, 1 - and the gateway does not guess them: guessing would be writing a dialect we do not speak.

An empty System value is a choice of its own: with nothing stored, the application falls back to its own default, which for most of them is the system.

The write happens in an inline script placed before the application's boot script, so the value is already in place when the app first reads it - and the chrome then simply inherits the document.

ng-m3-theme from the Softwarity ecosystem is the case that taught this: its service keeps system, light or dark under a key, and in system mode it clears the document's color-scheme on every run. Anything the gateway set was wiped a tick later. Writing the choice into that key instead lets the application apply it the way it already knows how, before its first paint - so here the System value is system, not empty.

Debugging it

What the route declared travels on the agent's own script tag, so one look at the served HTML says what the gateway thinks it was told:

<script defer src="/meerkat/page.js"
 data-scheme="select"
 data-scheme-mechanism="class"
 data-scheme-tag="body"
 data-scheme-light=""
 data-scheme-dark="dark"
 data-scheme-storage="theme"
 data-scheme-storage-light="light"
 data-scheme-storage-dark="dark"
 data-scheme-storage-auto="system"></script>

If a field you filled in is absent there, the route did not save it. If it is present and nothing happens, the tag is usually the answer.