<MeekoThemeToggle>
Accessible light/dark theme selector that does not jump on load and works without JavaScript, using themes from meeko.config.ts.
Deprecated since 1.8; removal in 2.0.
<MeekoThemeToggle>still works in 1.x and emitsMEEK121. Use<UIThemeToggle>from@meekojs/ui0.2 with its module enabled. It preserves props, snippets anddata-meeko-*markup. See Migration to 1.8.
With theme in meeko.config.ts, <MeekoThemeToggle> is the ready-to-use theme selector: a button cycling to the next choice (mode="cycle") or a radio group (mode="select"). Each snippet (light, dark, system or a custom theme) is that choice's icon.
It renders all icons and a rule Meeko adds to <head> shows the one for <html>'s data-theme-choice, written by the theme script before the first paint: the icon never jumps, even on a prerendered page. Without JavaScript, it is a <form> sending POST /_meeko/theme. No import is needed: the compiler recognizes it, and a project component with the same name takes priority (MEEK106).
Usage#
// meeko.config.ts
import { defineMeekoConfig } from '@meekojs/meeko/framework'
export default defineMeekoConfig({ theme: true })<!-- app/layouts/default.meeko -->
<template>
<header>
<MeekoThemeToggle class="toggle">
<snippet name="light"><IconSun /></snippet>
<snippet name="dark"><IconMoon /></snippet>
<snippet name="system"><IconMonitor /></snippet>
</MeekoThemeToggle>
</header>
<main><MeekoOutlet /></main>
</template>The button says “Tema: oscuro. Cambiar a sistema” (Theme: dark. Switch to system) and each click moves to the next choice.
Type#
<MeekoThemeToggle mode="cycle" bind:themes="['light', 'dark', 'system']" bind:labels="{ dark: 'Oscuro' }" class="toggle">
<snippet name="dark"><IconMoon /></snippet>
</MeekoThemeToggle>interface MeekoThemeToggleProps {
readonly mode?: 'cycle' | 'select'
readonly themes?: readonly ThemeChoice[]
readonly labels?: Partial<Record<ThemeChoice, string>>
readonly messages?: ThemeToggleMessages
readonly class?: string
readonly id?: string
}interface ThemeToggleMessages {
readonly cycle?: (current: string, next: string) => string
readonly unknown?: string
readonly group?: string
}type ThemeToggleSnippets = string extends ThemeChoice
? { readonly [name: string]: [] }
: { [Choice in ThemeChoice]?: [] }Props#
| Attribute | Type | Default | Description |
|---|---|---|---|
mode or bind:mode |
"cycle" | "select" |
"cycle" |
A button cycling to the next choice, or a radio group controlled with arrow keys. |
bind:themes |
readonly ThemeChoice[] |
themes and "system" if enabled |
Choices and their order, such as bind:themes="['light', 'dark']". |
bind:labels |
Partial<Record<ThemeChoice, string>> |
“Claro”, “Oscuro”, “Sistema” (Light, Dark, System) | Each choice's name; a custom theme without a label uses the theme name. |
bind:messages |
ThemeToggleMessages |
in Spanish | Accessible text: cycle(current, next) (the button's aria-label), unknown (“Cambiar tema”, Change theme) and group (“Tema”, Theme). |
class, id |
string |
none | For the root <form>. Other attributes do not pass through: wrap the tag if you need them. |
Snippets: one per choice, with its name (light, dark, system, sepia…), without parameters. Without a snippet, the choice's label renders. meeko check types themes, labels and snippets with configured themes.
Result#
A <form method="post" action="/_meeko/theme"> without visual styles:
mode="cycle": a<button>whosearia-labelstates the current and next choice, submitting the next. While the page does not know the choice (no cookie, on a prerendered page),aria-labelis neutral, “Cambiar tema” (Change theme), until hydration finishes.mode="select": arole="radiogroup"witharia-label(“Tema”, Theme) and onerole="radio"button per choice witharia-checkedanddata-meeko-theme-option.- Each icon goes in
<span data-meeko-theme-icon="light|dark|system|…">.
With JavaScript, submission is intercepted and the change is immediate with useTheme().set(), without navigation.
Examples#
A radio group#
<MeekoThemeToggle
mode="select"
class="theme-radios"
/>.theme-radios [role='radio'][aria-checked='true'] {
font-weight: 700;
}
/* Before hydration, without aria-checked, CSS reads the choice from <html>: */
[data-theme-choice='dark'] .theme-radios [data-meeko-theme-option='dark'] {
font-weight: 700;
}Only light and dark, with custom labels#
<MeekoThemeToggle
bind:themes="['light', 'dark']"
bind:labels="{ light: 'Day', dark: 'Night' }"
>
<snippet name="light">☀️</snippet>
<snippet name="dark">🌙</snippet>
</MeekoThemeToggle>Without system in the list, system mode shows the effective theme's icon, according to prefers-color-scheme.
Translate accessible text#
<MeekoThemeToggle
bind:labels="{ light: 'Light', dark: 'Dark', system: 'System' }"
bind:messages="{ cycle: (current, next) => `Theme: ${current}. Switch to ${next}`, unknown: 'Change theme', group: 'Theme' }"
/>A custom theme#
// meeko.config.ts
import { defineMeekoConfig } from '@meekojs/meeko/framework'
export default defineMeekoConfig({ theme: { themes: ['light', 'dark', 'sepia'] } })<MeekoThemeToggle>
<snippet name="light"><IconSun /></snippet>
<snippet name="dark"><IconMoon /></snippet>
<snippet name="sepia"><IconBook /></snippet>
<snippet name="system"><IconMonitor /></snippet>
</MeekoThemeToggle>Common mistakes#
Use it without theme in the configuration#
<!-- meeko.config.ts does not declare theme -->
<MeekoThemeToggle />meeko check reports MEEK111: <MeekoThemeToggle> sólo existe con theme en meeko.config.ts: añade theme: true (o sus opciones) para usarlo. If meeko check cannot read theme without executing the configuration (theme: variable), the component reports the same error when rendering.
export default defineMeekoConfig({ theme: true })A nonexistent mode#
<MeekoThemeToggle mode="toggle" />meeko check reports TS2322: Type '"toggle"' is not assignable to type '"cycle" | "select" | undefined'.
<MeekoThemeToggle mode="cycle" />A theme missing from the configuration#
<MeekoThemeToggle bind:themes="['light', 'night']" />With themes: ["light", "dark", "sepia"], meeko check reports TS2820: Type '"night"' is not assignable to type '"dark" | "light" | "sepia" | "system"'. Did you mean '"light"'?.
<MeekoThemeToggle bind:themes="['light', 'dark']" />Notes#
- Without JavaScript.
POST /_meeko/themeonly exists withthemeenabled, acceptsapplication/x-www-form-urlencodedup to 2 KB, checks the theme (400 INVALID_THEME) and requires anOriginmatching the application's (403 INVALID_ORIGIN). It writes the cookie and responds with303to the page, a path on this origin: never an open redirect. staticadapter. There is no server: it works with JavaScript, but without it, thePOSTreaches hosting, which rejects it.- No flash. The server writes the theme from the cookie; on a prerendered page, in
staticor in"system"without a client hint, the<head>script does so before the first paint. The icon rule is a<style>with the request's nonce or its hash on a static site: CSP without'unsafe-inline'is sufficient. - Hydration. HTML without a known choice has no
aria-checkedor specificaria-label; hydration renders the same and, on completion, each radio receives itsaria-checkedonce. There is never a mismatch. - Accessibility. In
mode="select", arrow keys,HomeandEndmove focus and choice with a rovingtabindex, like a native radio group. - Manual. For a custom selector,
useTheme()provides the same state.