Como hacer un selector claro/oscuro y guardar preferencia en JS en WordPress

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:

  1. Encolar la hoja de estilos que usa las variables CSS.
  2. Encolar el archivo JS principal.
  3. 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 🙂



Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *