Skip to content
Meeko
Type to search the guides and the API reference. Open it from any page with / or Ctrl K; move with the arrow keys and open a result with Enter.
Get started

Contents

On this page

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.

configuration Since 1.4.0

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#

ts
// 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
});
css
/* 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#

MeekoConfig.theme

ts
readonly theme?: boolean | ThemeConfig

ThemeConfig

ts
interface 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#

ts
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#

meeko
<!-- 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.

ts
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#

Incorrectts
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.

Correctts
import { defineMeekoConfig } from "@meekojs/meeko/framework";
export default defineMeekoConfig({ theme: { themes: ["sepia", "dark"], system: false } });

A default not in themes#

Incorrectts
export default defineMeekoConfig({ theme: { default: "sepia" } });

…/meeko.config.ts: theme: default debe ser uno de los temas de themes (light, dark).

Correctts
import { defineMeekoConfig } from "@meekojs/meeko/framework";
export default defineMeekoConfig({ theme: { themes: ["light", "dark", "sepia"], default: "sepia" } });

The theme in document#

Incorrectts
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.

Correctts
import { defineMeekoConfig } from "@meekojs/meeko/framework";
export default defineMeekoConfig({ theme: true });

<MeekoThemeToggle> without theme#

Incorrectmeeko
<!-- 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.

Correctts
import { defineMeekoConfig } from "@meekojs/meeko/framework";
export default defineMeekoConfig({ theme: true });

Notes#

  • CSP. In SSR, the script and icon <style> carry the nonce (from DocumentOptions.nonce or, since 1.7, generated by Meeko with server.contentSecurityPolicy); on prerendered and static pages, their hashes enter the CSP. color-scheme is applied through CSSOM, which a CSP without 'unsafe-inline' in style-src permits.
  • 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 not HttpOnly and 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. If theme held options for a module with configKey: "theme", the error explains it: give the module a different configKey (see docs/migration-1.4.md).

Was this page helpful?

Add a comment

It is sent when you choose “Yes” or “No”. Only the page, your answer and the comment are stored: no cookies and no personal data.

0 of 2000 characters

Edit on GitHub: docs/en/reference/config/config-theme.md