Contents
Introducción
Este artículo explica paso a paso cómo implementar un selector claro/oscuro en WordPress y cómo guardar la preferencia del usuario mediante JavaScript para que persista entre páginas y sesiones. Se incluyen buenas prácticas: uso de variables CSS, detección de la preferencia del sistema, prevención del parpadeo (FOIT), accesibilidad y la integración correcta dentro de un tema o plugin de WordPress.
Requisitos y objetivos
- Compatibilidad con navegadores modernos (soporte para localStorage y prefers-color-scheme).
- Aplicar temas mediante variables CSS (fácil mantenimiento y transiciones suaves).
- Persistencia de la preferencia con localStorage (alternativa: cookies).
- Evitar parpadeo inicial mostrando el tema correcto desde el primer render.
- Integración con el flujo de WordPress (enqueue de scripts y estilo, impresión de un script inline en ltheadgt).
Conceptos clave
- Variables CSS: permiten cambiar colores globalmente sin duplicar reglas.
- data-theme en el elemento raíz (document.documentElement): selector claro para estilos [data-theme=dark].
- prefers-color-scheme: detectar la preferencia del sistema como valor por defecto.
- localStorage: almacenar la preferencia del usuario de forma persistente.
- Script inline en ltheadgt: aplicar la preferencia antes de que se cargue CSS para evitar FOIT.
Implementación paso a paso
1) CSS: definir variables y reglas para los temas
Se recomienda usar variables en :root para el tema claro y sobreescribirlas cuando data-theme=dark esté presente en el elemento raíz. Así se evita tocar muchas reglas y se mantiene el control centralizado.
:root { --bg: #ffffff --text: #111827 --link: #1a73e8 --muted: #6b7280 --accent: #2563eb --transition-speed: 200ms } [data-theme=dark] { --bg: #0b1220 --text: #e6eef8 --link: #8ab4ff --muted: #9aa4b2 --accent: #60a5fa } / Usar las variables en el CSS del tema / :root, [data-theme] { background-color: var(--bg) color: var(--text) transition: color var(--transition-speed) ease, background-color var(--transition-speed) ease } a { color: var(--link) } .small-muted { color: var(--muted) }
2) HTML del control (marcado accesible)
Un botón simple con atributos ARIA para indicar estado puede colocarse en la cabecera o en el personalizador del tema.
3) JavaScript: lógica para alternar y persistir preferencia
Se compone de dos partes: un script inline que se imprime en ltheadgt para aplicar el tema antes del render y un script principal que maneja el evento del botón y guarda la preferencia en localStorage.
Script inline para evitar FOIT (añádelo en ltheadgt mediante wp_head o print_inline_script):
(function() { try { var stored = localStorage.getItem(wp-theme-preference) if (stored === dark stored === light) { document.documentElement.setAttribute(data-theme, stored) return } var prefersDark = window.matchMedia window.matchMedia((prefers-color-scheme: dark)).matches if (prefersDark) { document.documentElement.setAttribute(data-theme, dark) } else { document.documentElement.setAttribute(data-theme, light) } } catch (e) { // Si localStorage no está disponible, confiar en la preferencia del sistema (no hacer nada) } })()
Script principal (archivo JS separado):
(function() { var toggle = document.getElementById(theme-toggle) if (!toggle) return function getStoredTheme() { try { return localStorage.getItem(wp-theme-preference) } catch (e) { return null } } function storeTheme(theme) { try { localStorage.setItem(wp-theme-preference, theme) } catch (e) { // silenciar errores de almacenamiento } } function applyTheme(theme) { if (!theme) return document.documentElement.setAttribute(data-theme, theme) if (toggle) { toggle.setAttribute(aria-pressed, theme === dark ? true : false) // Actualizar icono si lo deseas var icon = document.getElementById(theme-toggle-icon) if (icon) icon.textContent = theme === dark ? 🌙 : ☀️ } } // Inicializar estado del botón según lo almacenado o preferencia del sistema (function init() { var stored = getStoredTheme() if (stored) { applyTheme(stored) } else { var prefersDark = window.matchMedia window.matchMedia((prefers-color-scheme: dark)).matches applyTheme(prefersDark ? dark : light) } })() toggle.addEventListener(click, function() { var current = document.documentElement.getAttribute(data-theme) light var next = current === dark ? light : dark applyTheme(next) storeTheme(next) // Si usas analytics o eventos, puedes enviar aquí el cambio }) })()
4) Integración en WordPress (enqueue y script inline en ltheadgt)
Coloca este código en functions.php de tu tema o en un plugin específico del sitio. El objetivo es:
- Encolar la hoja de estilos que usa las variables CSS.
- Encolar el archivo JS principal.
- Imprimir el script inline (el pequeño de inicialización) en ltheadgt para prevenir FOIT.
// En functions.php o en un plugin function mi_tema_enqueue_theme_toggle() { // Encolar CSS del tema (ajusta handle y ruta) wp_enqueue_style(mi-tema-style, get_stylesheet_uri(), array(), wp_get_theme()->get(Version)) // Encolar script principal depender de jquery es opcional wp_enqueue_script( mi-tema-theme-toggle, get_template_directory_uri() . /assets/js/theme-toggle.js, array(), 1.0, true ) // Obtener el script small que aplicará el tema antes de renderizar inline = (function(){try{var s=localStorage.getItem(wp-theme-preference)if(s===darks===light){document.documentElement.setAttribute(data-theme,s)return}var p=window.matchMediawindow.matchMedia((prefers-color-scheme: dark)).matchesif(p){document.documentElement.setAttribute(data-theme,dark)}else{document.documentElement.setAttribute(data-theme,light)}}catch(e){}})() // Imprimir inline en head wp_register_script(mi-tema-inline-theme, ) wp_enqueue_script(mi-tema-inline-theme) wp_add_inline_script(mi-tema-inline-theme, inline) } add_action(wp_enqueue_scripts, mi_tema_enqueue_theme_toggle)
Notas:
- Si usas un constructor o plugin de caché, asegúrate de que el script inline no sea eliminado ni combinado de forma que deje de ejecutarse antes del CSS.
- El script inline debe cargarse lo más pronto posible (antes del CSS) para evitar que se muestre el tema incorrecto al cargar la página.
Accesibilidad y detalles UX
- Usa atributos ARIA: aria-pressed en el botón para que lectores de pantalla puedan interpretar el estado.
- Asegura contraste suficiente en ambos temas (contrasta texto y fondo según WCAG).
- Ofrece prefers-reduced-motion para usuarios que no desean transiciones largas.
- Permite teclado: el botón debe ser alcanzable con Tab y activable con Enter/Espacio.
- Si tu sitio tiene persistencia de sesión en servidor (por ejemplo, perfiles de usuario), puedes sincronizar la preferencia guardándola en meta del usuario vía AJAX y restaurándola por PHP para usuarios autenticados.
Variantes y opciones avanzadas
- Sin JavaScript: imposible cambiar dinámicamente, pero puedes usar media query prefers-color-scheme para servir estilos por defecto.
- Guardar en PHP (usuarios autenticados): guardar meta user_meta y aplicar theme en servidor para que el HTML generado ya tenga el atributo correcto.
- Usar cookies en lugar de localStorage: útil si quieres que el valor también esté disponible en servidores o plugins que leen cookies.
- Clases en lugar de data-attribute: algunas integraciones prefieren añadir una clase como .theme-dark al elemento raíz la técnica es equivalente.
Problemas comunes y soluciones
- Parpadeo inicial (FOIT): se soluciona con el script inline que aplica el tema antes de que se cargue el CSS.
- Caching/CDN: si un CSS generado depende del tema, mejor usar variables CSS y aplicar el atributo en HTML para evitar servir archivos distintos cached.
- Plugins que alteran el DOM o el head: asegúrate de que tu script inline se mantenga y no sea desplazado abajo por otros plugins imprimirlo con wp_add_inline_script es la mejor práctica.
- LocalStorage deshabilitado: el script contiene try/catch para comportarse de forma segura en estos casos y caer en la preferencia del sistema.
Código completo de ejemplo
A continuación, un conjunto de ejemplos completos para copiar/pegar y adaptar: CSS, JS principal y PHP para enqueue.
/ assets/css/theme-vars.css / :root { --bg: #ffffff --text: #111827 --muted: #6b7280 --link: #1a73e8 --transition: 150ms } [data-theme=dark] { --bg: #0b1220 --text: #e6eef8 --muted: #9aa4b2 --link: #8ab4ff } html, body { background: var(--bg) color: var(--text) transition: background var(--transition) ease, color var(--transition) ease } button#theme-toggle { background: transparent border: 1px solid var(--muted) color: var(--text) padding: 6px 10px border-radius: 6px }
/ assets/js/theme-toggle.js / (function() { var toggle = document.getElementById(theme-toggle) if (!toggle) return function getStoredTheme() { try { return localStorage.getItem(wp-theme-preference) } catch (e) { return null } } function storeTheme(theme) { try { localStorage.setItem(wp-theme-preference, theme) } catch (e) {} } function applyTheme(theme) { if (!theme) return document.documentElement.setAttribute(data-theme, theme) toggle.setAttribute(aria-pressed, theme === dark ? true : false) var icon = document.getElementById(theme-toggle-icon) if (icon) icon.textContent = theme === dark ? 🌙 : ☀️ } (function init() { var s = getStoredTheme() if (s) { applyTheme(s) } else { var pref = window.matchMedia window.matchMedia((prefers-color-scheme: dark)).matches applyTheme(pref ? dark : light) } })() toggle.addEventListener(click, function() { var current = document.documentElement.getAttribute(data-theme) light var next = current === dark ? light : dark applyTheme(next) storeTheme(next) }) })()
/ functions.php: encolar CSS, JS y añadir script inline en head / function mi_tema_enqueue_theme_toggle() { // CSS con variables wp_enqueue_style(mi-tema-vars, get_template_directory_uri() . /assets/css/theme-vars.css, array(), 1.0) // Script principal wp_enqueue_script(mi-tema-theme-toggle, get_template_directory_uri() . /assets/js/theme-toggle.js, array(), 1.0, true) // Inline script para aplicar tema antes del render y evitar FOIT inline = (function(){try{var s=localStorage.getItem(wp-theme-preference)if(s===darks===light){document.documentElement.setAttribute(data-theme,s)return}var p=window.matchMediawindow.matchMedia((prefers-color-scheme: dark)).matchesif(p){document.documentElement.setAttribute(data-theme,dark)}else{document.documentElement.setAttribute(data-theme,light)}}catch(e){}})() // Registrar e inyectar inline wp_register_script(mi-tema-inline, ) wp_enqueue_script(mi-tema-inline) wp_add_inline_script(mi-tema-inline, inline) } add_action(wp_enqueue_scripts, mi_tema_enqueue_theme_toggle)
Conclusión
Con esta aproximación se obtiene un selector claro/oscuro robusto: uso de variables CSS para simplicidad y rendimiento, persistencia con localStorage, detección de la preferencia del sistema, y una integración segura en WordPress mediante enqueue y un script inline para evitar parpadeos. Implementando buenas prácticas de accesibilidad y comprobando la interacción con cachés y plugins, el resultado es una experiencia consistente y agradable para los usuarios.
|
Acepto donaciones de BAT's mediante el navegador Brave 🙂 |