theme
Flicker-free light, dark or custom application themes, with the choice in a cookie, the theme on html from the first byte and useTheme in components.
theme enables Meeko's light/dark theme (and custom themes). The user's choice is stored in a cookie; the server writes the theme to <html> from the first byte, and when it cannot know it (a prerendered page, the static adapter, "system" mode without a client hint), a minimal blocking script in <head> applies it before the first render. Components read and change the theme with useTheme() and <MeekoThemeToggle>.
Without theme, nothing changes: no script, cookie, headers or POST /_meeko/theme.
Usage#
// meeko.config.ts
import { defineMeekoConfig } from "@meekojs/meeko/framework";
export default defineMeekoConfig({
theme: true, // light and dark, "system" by default, data-theme and the meeko-theme cookie
});/* app/styles/main.css */
:root { color-scheme: light; --bg: #fff; --ink: #111; }
:root[data-theme="dark"] { color-scheme: dark; --bg: #111; --ink: #eee; }
@media (prefers-color-scheme: dark) {
:root:not([data-theme]) { color-scheme: dark; --bg: #111; --ink: #eee; }
}Type#
readonly theme?: boolean | ThemeConfiginterface ThemeConfig {
readonly themes?: readonly string[]
readonly default?: string
readonly system?: boolean | { readonly light: string; readonly dark: string }
readonly darkThemes?: readonly string[]
readonly attribute?: 'class' | `data-${string}`
readonly colorScheme?: boolean
readonly cookie?: string
readonly storageKey?: string | false
readonly clientHints?: boolean
}Options#
theme accepts true (the defaults) or an object:
| Option | Type | Default | Description |
|---|---|---|---|
themes |
readonly string[] |
["light", "dark"] |
Themes: letters, digits, - or _, and not "system". |
default |
string |
"system", or the first theme with system: false |
The choice for users who have not chosen yet. |
system |
boolean | { light, dark } |
true |
The "system" mode, which follows prefers-color-scheme. true uses light and dark; an object chooses other themes; false disables it. |
darkThemes |
readonly string[] |
the dark theme from system, or dark |
Themes that receive color-scheme: dark. |
attribute |
"class" | "data-…" |
"data-theme" |
Where the theme goes on <html>: a data-* attribute or the name as a class. |
colorScheme |
boolean |
true |
Applies color-scheme to <html> through CSSOM. |
cookie |
string |
"meeko-theme" |
The choice cookie. |
storageKey |
string | false |
the value of cookie |
The localStorage key that notifies other tabs; false uses only the cookie. |
clientHints |
boolean |
true |
Requests and reads Sec-CH-Prefers-Color-Scheme to resolve "system" on the server. |
Result#
| Case | Who sets the theme on <html> |
|---|---|
| SSR with the cookie | The server, from the first byte. |
SSR in "system" with the client hint (Chromium) |
The server. |
SSR in "system" without the hint (Firefox, Safari) |
The <head> script, before the first render. |
Prerendered page or static adapter |
The <head> script, using the cookie or localStorage. |
SSR responses carry Accept-CH: Sec-CH-Prefers-Color-Scheme (and Critical-CH in "system") and Vary: Cookie, Sec-CH-Prefers-Color-Scheme.
Examples#
Custom themes#
import { defineMeekoConfig } from "@meekojs/meeko/framework";
export default defineMeekoConfig({
theme: { themes: ["day", "night", "sepia"], system: { light: "day", dark: "night" }, darkThemes: ["night"], attribute: "class" },
});A theme selector#
<!-- app/components/ThemePicker.meeko -->
<script lang="ts">
const { theme } = useTheme();
</script>
<template>
<select model="theme">
<option value="system">System</option>
<option value="light">Light</option>
<option value="dark">Dark</option>
</select>
</template>Or, without writing anything, <MeekoThemeToggle />, which also works without JavaScript through a form.
One cookie per application in development#
import { defineMeekoConfig } from "@meekojs/meeko/framework";
// Cookies are per host, not per port: two apps on localhost would share meeko-theme.
export default defineMeekoConfig({ theme: { cookie: "tienda-theme" } });Common mistakes#
Custom themes with system enabled#
export default defineMeekoConfig({ theme: { themes: ["sepia", "dark"] } });meeko dev, meeko build, meeko check and meeko routes stop with …/meeko.config.ts: theme: system necesita los temas "light" y "dark"; asigne otros nombres con system: { light, dark } o desactívelo con system: false.
import { defineMeekoConfig } from "@meekojs/meeko/framework";
export default defineMeekoConfig({ theme: { themes: ["sepia", "dark"], system: false } });A default not in themes#
export default defineMeekoConfig({ theme: { default: "sepia" } });…/meeko.config.ts: theme: default debe ser uno de los temas de themes (light, dark).
import { defineMeekoConfig } from "@meekojs/meeko/framework";
export default defineMeekoConfig({ theme: { themes: ["light", "dark", "sepia"], default: "sepia" } });The theme in document#
export default defineMeekoConfig({ document: { theme: { themes: ["light", "dark"] } } as never });…/meeko.config.ts: document.theme no es una opción de meeko.config.ts; use theme en el nivel superior.
import { defineMeekoConfig } from "@meekojs/meeko/framework";
export default defineMeekoConfig({ theme: true });<MeekoThemeToggle> without theme#
<!-- without theme in meeko.config.ts -->
<template><MeekoThemeToggle /></template>meeko check reports MEEK111 and, at runtime, the component throws MEEK111: <MeekoThemeToggle> necesita theme en meeko.config.ts (theme: true) y una aplicación de Meeko en marcha.
import { defineMeekoConfig } from "@meekojs/meeko/framework";
export default defineMeekoConfig({ theme: true });Notes#
- CSP. In SSR, the script and icon
<style>carry the nonce (fromDocumentOptions.nonceor, since1.7, generated by Meeko withserver.contentSecurityPolicy); on prerendered andstaticpages, their hashes enter the CSP.color-schemeis applied through CSSOM, which a CSP without'unsafe-inline'instyle-srcpermits. - Security. The server only accepts a theme from
themes(or"system") in the cookie; any other value is ignored and never written to the HTML. The cookie is notHttpOnlyand contains nothing private. - Cache. SSR responses vary by cookie: a CDN caching them must include the theme cookie in the key, or it will serve one user's theme to another. Prerendered pages do not vary by theme.
- Hydration. The browser hydrates with server values and applies its own afterward, before
onMounted: there is never a mismatch. - Before
1.4. Ifthemeheld options for a module withconfigKey: "theme", the error explains it: give the module a differentconfigKey(seedocs/migration-1.4.md).