Dark Mode: Buenas Prácticas para Crear un Modo Oscuro Efectivo

16 Mayo, 2022 - 9 minutos de lectura

En los últimos años, se podría decir que se ha vuelto un estándar crear sitios web que en el diseño de su interfaz soporte el modo oscuro. Cabe aclarar que esta publicación no está enfocada en aspectos de diseño ni en teoría de color, sino en la forma técnica (código) para hacer que el modo oscuro sea totalmente funcional para nuestros usuarios.

Más allá de las razones estéticas del modo oscuro, se ha llegado a considerar una característica de accesibilidad de usuario, ya que ofrece una solución a la fatiga visual en entornos de poca luz o incluso problemas de migraña provocados por el alto brillo de las pantallas, especialmente con interfaces claras.

Origen del modo oscuro

El modo oscuro tiene su origen en los sistemas operativos móviles como Android e iOS, e inicialmente se tuvo pensado como una característica para poder ahorrar el consumo de batería, debido a que muchos dispositivos estaban empezando a emplear pantallas OLED o AMOLED, ya que el funcionamiento de estas pantallas permite que cada píxel se ilumine de manera independiente y para representar el color negro, los píxeles simplemente no se iluminan.

Es por eso, que en estos sistemas operativos tienen mayor adopción del modo oscuro, y luego sistemas operativos de escritorio como Windows, MacOS y algunas distribuciones Linux, poco a poco fueron adoptando este enfoque.

Pero, ¿qué pasa con las páginas web?. En un sitio web no existe una forma automática de alternar entre el modo claro al modo oscuro, al menos no de una forma explícitamente definida en su diseño o desarrollo.

Aunque hay extensiones de navegador o bibliotecas de JavaScript que permiten alternar entre estos ‘temas’, en algunos casos no ofrecen buenos resultados debido a algunos colores utilizados en las páginas web, que no llegan a tener un muy buen aspecto, debido a las técnicas de teoría del color empleados por estas herramientas.

Usando Darkmode.js para establecer el modo oscuro
Usando Darkmode.js para establecer el modo oscuro.

Por ejemplo en la imágen anterior estableciendo el modo oscuro usando la librería Darkmode.js, los colores se invierten ya que utiliza la propiedad CSS mix-blend-mode para ‘aplicar el modo oscuro’. Podemos solucionar estos efectos secundarios usando otra propiedad CSS isolation, pero al final creo que es mucho más trabajo del necesario.

Debido a la evolución del estándar de CSS, es mucho más fácil actualmente alternar entre el modo claro y modo oscuro de nuestro sitio web, usando la media query prefers-color-scheme y de esta forma somos nosotros mismos quiénes controlamos y ofrecemos a nuestros usuarios un mejor aspecto visual de nuestra página y no herramientas de terceros.

Formas de activar el modo oscuro

Debido a la adopción de los sistemas operativos, actualmente hay 2 formas de activar el modo oscuro de nuestra página web:

Activando el modo oscuro mediante el sistema operativo

Gracias a la media query prefers-color-scheme, podemos detectar si la configuración del sistema operativo del usuario (ya sea móvil o de escritorio) admite el modo oscuro.

La media query es parte del estándar de Media Queries Level 5 y tiene muy buen soporte en los navegadores, pero este también depende si el sistema operativo soporta la preferencia entre el modo oscuro y claro.

Soporte de la media query prefers-color-scheme
Soporte de la media query prefers-color-scheme.

Puedes ver los datos más actualizados de soporte en los diferentes navegadores en el siguiente enlace: https://caniuse.com/prefers-color-scheme.

Esta media query posee 2 valores:

  • light: Indica que el tema del sistema operativo del usuario es claro.
  • dark: Cuándo esta comprobación resulta verdadera, se aplican los colores que hayamos definido en el diseño de nuestro sitio web para el modo oscuro.

Por ejemplo:

/* Estilos para el modo claro */
:root {
  --bg: #fffacd;
  --text: #333;
}

/* Estilos en modo oscuro */
@media (prefers-color-scheme: dark) {
  :root {
    --bg: #3b4252;
    --text: #eee;
  }
}

body {
  background: var(--bg);
  color: var(--text);
}

¿Cómo podemos simular este comportamiento en el navegador?

Obviamente a la hora de desarrollar no sería muy cómodo tener que desactivar y activar el modo oscuro en las configuraciones de nuestro sistema operativo, en Chrome y en los navegadores basados en Chromium, es muy fácil simular esta característica.

Para activarla debemos acceder a las herramientas para desarrolladores presionando la tecla F12, la combinación de teclas Ctrl+Shift I o llendo a los 3 puntos Abrir Opciones Chrome ubicados en la parte superior derecha, luego a Más herramientas y por último a Herramientas del desarrollador, veremos una ventana similar a esta:

Herramientas del desarrollador de Chrome
Herramientas del desarrollador de Chrome.

Luego de eso, debemos presionar la combinación de teclas Ctrl+Shift P, escribimos en el cuadro de búsqueda Rendering y luego hacemos clic en Show Rendering (En caso de que tengas el idioma de las DevTools de tu navegador en inglés).

Búscando la opción Rendering en las DevTools
Búscando la opción Rendering en las DevTools.

Aparecerá esta ventana.

Ventana con las opciones de Rendering
Ventana con las opciones de Rendering.

Bajamos y veremos la opción Emulate CSS media feature prefers-color-scheme y allí podemos seleccionar el modo oscuro para poder simular y probar nuestros diseños sin necesidad de cambiar la configuración de nuestro sistema operativo.

Simulando la Media Query prefers-color-scheme
Simulando la Media Query prefers-color-scheme.

Aunque este enfoque es muy bueno, tiene un pequeño inconveniente: no dejar elegir al usuario el tema preferido, por ejemplo, hay usuarios que podrían preferir evitar el modo oscuro en algunas páginas web y tendrían que remover la configuración a nivel del sistema operativo. Además que no gestiona de ninguna manera la persistencia de las preferencias del usuario, es decir, si por alguna razón se desactiva el modo oscuro, nuestra página web también se verá afectada, mostrando el tema claro.

Activando el modo oscuro con JavaScript

Para poder proveer una forma fácil de alternar entre el tema claro y el modo oscuro en nuestro sitio web tendremos que hacer uso de JavaScript. En esta sección te explicaré paso a paso cómo crear un botón que alterne (toggle) entre ambos temas y cómo respetar la preferencia de nuestros usuarios dependiendo de la configuración del sistema operativo.

Creando un botón

Hay muchas maneras de diseñar un botón para alternar entre el modo claro y el oscuro, por razones de simplicidad no incluiré ningún estilo al botón.

<button id="toogle" type="button">Alternar tema</button>

Estilos básicos

Añadimos estilos necesarios a nuestra página web, para que luzca bien en ambos modos.

/* Estilos para el modo claro */
:root {
  --bg: #fffacd;
  --text: #333;
}

/* Estilos en modo oscuro */
:root.dark {
  --bg: #3b4252;
  --text: #eee;
}

body {
  background: var(--bg);
  color: var(--text);
  /* Transición entre los colores en modo claro y oscuro */
  transition: background, color 300ms ease;
}

Como habrás notado se ha omitido el uso de la media query prefers-color-scheme, debido a las razones explicadas en las sección anterior.

Detectando la preferencia del color

Pero podemos usar la media query prefers-color-scheme para detectar qué tipo de configuración tiene el usuario en el sistema operativo de su dispositivo.

// Detectamos si el usuario tiene habilitado el modo oscuro
const getScheme = window.matchMedia('(prefers-color-scheme: dark)')

Utilizando el método matchMedia del objeto window, podemos obtener fácilmente la preferencia del tema elegido por los usuarios. Por ahora no hace mucho, pero nos será útil más adelante para alternar automáticamente entre los temas.

¿Lo sabías?

Con el método matchMedia podemos identificar cualquier tipo de media query para aplicar efectos o realizar alguna acción determinada, por ejemplo: window.matchMedia(‘min-width: 720px’).

Devuelve un nuevo objeto denominado MediaQueryList.

Creando una función para alternar de tema

Podemos automatizar el proceso de alternación utilizando una función, que establezca a la etiqueta <html> la clase CSS que hayamos definido para nuestro tema oscuro (en este caso dark):

function toggleDarkMode(state) {
  document.documentElement.classList.toggle('dark', state)
}

La función toggleDarkMode recibe un parámetro state, que más adelante se lo podemos pasar en la llamada de la función, y debido a que el método toggle() del objeto classList recibe un segundo parámetro booleano opcional que en caso de ser true añade la clase y en caso contrario la elimina, podremos alternar fácilmente entre cada tema.

¿Porqué aplicar la clase a la etiqueta html?

En mi experiencia, es preferible establecer clases que realizan acciones globales a la etiqueta <html>, ya que en el proceso de análisis del documento y construcción del DOM esta es la primer etiqueta en ser analizada aportándonos así muchas ventajas con respecto a aplicar clases a la etiqueta <body>.

Luego podemos llamar a la función de la siguiente manera:

toggleDarkMode(getScheme.matches)

Aquí es dónde nos es útil el objeto MediaQueryList que guardamos en la constante getScheme, ya que podemos utilizar la propiedad matches que devuelve un valor booleano (true o false) dependiendo de la preferencia que coincida con la media query que estamos evaluando se añadirá o removerá la clase dark.

Lo que tenemos hasta ahora, es lo equivalente a que utilizaramos la media query prefers-color-scheme en nuestros estilos CSS. Ya que al cargar nuestra página web, en caso de que el usuario tenga activado el modo oscuro se aplicará la clase dark a la etiqueta <html>. Pero aún nos falta añadir funcionalidad a nuestro botón.

Detectando los cambios de las preferencias de los usuarios

Gracias a que el objeto MediaQueryList tiene un método addListener() disponible, podemos utilizarlo para establecer una ‘escucha’ a la configuración de los usuarios a nivel de sistema operativo.

// Escuchamos los cambios en las configuración
// del sistema operativo para alternar de tema
getScheme.addListener(evt => toggleDarkMode(evt.matches))

Volvemos a llamar a la función toggleDarkMode para alternar automáticamente el tema, y debido a que podemos obtener el evento del método addListener (en mi caso lo he llamado evt) el cuál también tiene una propiedad matches que utilizaremos como parámetro dentro de la función para acceder a su valor booleano.

Añadiendo funcionalidad al botón

Ahora podemos utilizar nuestro botón para ofrecer a los usuarios una forma para alternar nuestro tema.

// Seleccionamos el botón
const toggleBtn = document.getElementById('toggle')

toggleBtn.addEventListener('click', () => {
  document.documentElement.classList.toggle('dark')
})

Ya hemos establecido un botón para alternar de tema y también hemos creado una función que detecta las preferencias de nuestro usuarios para alternar de igual manera al modo oscuro.

A continuación puedes ver el código JavaScript completo:

/* Código JavaScript completo */

// Seleccionamos el botón
const toggleBtn = document.getElementById('toggle')

// Detectamos si el usuario tiene habilitado el modo oscuro
const getScheme = window.matchMedia('(prefers-color-scheme: dark)')

toggleBtn.addEventListener('click', () => {
  document.documentElement.classList.toggle('dark')
})

function toggleDarkMode(state) {
  document.documentElement.classList.toggle('dark', state)
}

toggleDarkMode(getScheme.matches)

// Escuchamos los cambios en las configuración del
// del sistema operativo para alternar de tema
getScheme.addListener(evt => toggleDarkMode(evt.matches))

Ahora lo que debemos arreglar es hacer que la preferencia que haya elegido nuestro usuario sea persistente, por ejemplo si elige el tema oscuro, al salir y entrar nuevamente a nuestra página web ésta no almacenará su elección. Pero vamos a arreglarlo.

Haciendo el modo oscuro persistente

Antes de las nuevas API’s de HTML 5, la única forma de almacenar información proveniente de los usuarios era a través de cookies, pero debido a las limitaciones y que el manejo de los datos de las cookies se hace a nivel de servidor, es por eso que utilizaremos la API de localStorage para poder almacenar la preferencia del tema que haya elegido nuestro usuario.

Para ello haremos unas pequeñas modificaciones en el código anterior:

// Establecemos una variable global
let darkModeState = false

// Seleccionamos el botón
const toggleBtn = document.getElementById('toggle')

// Detectamos si el usuario tiene habilitado el modo oscuro
const getScheme = window.matchMedia('(prefers-color-scheme: dark)')

function toggleDarkMode(state) {
  document.documentElement.classList.toggle('dark', state)

  // Establecemos el valor de la variable global
  // con el valor del parámetro state
  darkModeState = state
}

// Creamos una nueva función para almacenar el valor
// en localStorage
function setSchemeState(state) {
  localStorage.setItem('dark-mode', state)
}

// toggleDarkMode(getScheme.matches)
// Reemplazamos el valor obtenido por la variable getScheme
// por el valor que esté guardado en localStorage, para que
// al cargar la página verifique el valor almacenado en
// localStorage
toggleDarkMode(localStorage.getItem('dark-mode') === 'true')

// Escuchamos los cambios en las configuración del
// del sistema operativo para alternar de tema
getScheme.addListener(evt => toggleDarkMode(evt.matches))

// Movemos el manejador del evento click del botón al final
toggleBtn.addEventListener('click', () => {
  // Negamos el valor guardado dentro de la función toggleDarkMode
  // a la variable global darkModeState y se lo asignamos así misma
  darkModeState = !darkModeState

  // document.documentElement.classList.toggle('dark')
  // Llamamos a la funciones toggleDarkMode para alternar el tema
  // y setSchemeState para guardar el valor en localStorage
  toggleDarkMode(darkModeState)
  setSchemeState(darkModeState)
})

Dentro de la función setSchemeState, hacemos uso de localStorage, con el método setItem() establecemos el valor. Este método recibe 2 parámetros, el primero es el nombre del valor en mi caso lo llamé dark-mode, pero puedes usar el nombre que prefieras; el segundo es el valor a almacenar, aquí guardamos el valor del estado del tema (true o false).

Y de esta forma ya tendremos nuestro botón totalmente funcional, además sin corromper la funcionalidad de escuchar los cambios de configuración del sistema operativo, los cuales también serán almacenados en localStorage.

Alternando entre el modo claro y el modo oscuro con nuestro botón.

Pero ahora debemos resolver otro problema, y es que al tener el modo oscuro activado, al navegar hacia otra página de nuestro sitio web se producirá un pequeño destello.

Parpadeo entre el modo claro y el modo oscuro al navegar a otras páginas.

¿Cómo evitar el parpadeo entre páginas?

La razón por la que ocurre este comportamiento, es por cómo funciona nuestro código, al cargar la página se tiene que evaluar el valor guardado en localStorage y eso le toma tiempo de ejecutar al navegador dependiendo que tan optimizada esté nuestra página web.

Para evitar este parpadeo, debemos agregar un pequeño código JavaScript en línea dentro de la etiqueta <head> y antes de la importación de nuestra hoja de estilos:

<head>
  <script>
    if (localStorage.getItem('dark-mode') === 'true') {
      document.documentElement.classList.add('dark')
    }
  </script>
  <link rel="stylesheet" href="css/style.css" />
</head>

Así cada vez que cargue cada página, se evaluará la condición y en caso de ser verdadera agregará inmediatamente la clase CSS dark a la etiqueta <html>, para cuándo llegue el momento de descargar y analizar los estilos CSS la clase ya esté aplicada y no se produzca este destello.

Ahora ya hemos arreglado cada detalle relacionado al cambio de tema de nuestro sitio web, para que nuestros usuarios puedan elegir con qué tema prefieren visualizar nuestra página web.

Bonus: Creando un atajo de teclado

Si quieres ir un poco más allá en términos de accesibilidad y ofrecer una forma rápida de alternar entre cada modo de color a tus usuarios, la mejor solución es crear un atajo de teclado.

Debemos tomar en cuenta elegir muy bien el atajo de teclado que vayamos a crear para no entrar en conflicto con cualquier otro atajo ya creado, ya que el navegador provee una serie de atajos de teclado ya predefinidos.

Para crear el atajo de teclado, estableceremos un escuchador de eventos al objeto window, el evento que utilizaremos será keydown.

Podemos extender el siguiente código con el que ya creamos anteriormente:

window.addEventListener('keydown', e => {
  // Detectamos si las teclas Alt, Shift y D están presionadas
  if (e.altKey === true && e.shiftKey === true && e.key === 'D') {
    darkModeState = !darkModeState

    toggleDarkMode(darkModeState)
    setSchemeState(darkModeState)
  }
})

Como podrás notar el código es muy similar al que establecimos en nuestro botón, la única diferencia es la acción. El atajo de teclado que establecí es Alt + Shift D (el mismo de mi sitio web), ya que no es un atajo que esté utilizado por el navegador y resulta muy cómodo de utilizar.

Con unas pocas líneas de código, pudimos construir un ‘componente’ que alterna entre el modo claro y el modo oscuro de forma fácil para nuestros usuarios, que además también almacena su preferencia y ofrecemos una opción rápida mediante un atajo de teclado. Espero puedas utilizar este enfoque en tus futuros proyectos.


Si te ha gustado este artículo, podrías invitarme a un café, te lo agradecería muchísimo.

Invítame a un café

¡Comparte este artículo!

Otras publicaciones

Mira otras publicaciones