Me gusta cuando los sitios web tienen una opción de modo oscuro. El modo oscuro facilita la lectura de las páginas web y ayuda a que mis ojos se sientan más relajados. Muchos sitios web, incluidos YouTube y Twitter, ya lo han implementado, y estamos comenzando a ver que también se filtra en muchos otros sitios.
En este tutorial, vamos a construir un conmutador que permita a los usuarios cambiar entre los modos claro y oscuro, usando un <ThemeProvider
contenedor de la biblioteca de componentes con estilo. Crearemos un useDarkMode
gancho personalizado, que soporta el prefers-color-scheme
consulta de medios para establecer el modo de acuerdo con la configuración del esquema de color del sistema operativo del usuario.
Si eso suena difícil, ¡te prometo que no lo es! Profundicemos y hagamos que suceda.
Ver la pluma
Alternar el modo día / noche con React y ThemeProvider de Maks Akymenko (@maximakymenko)
en CodePen.
Vamos a configurar las cosas
Usaremos create-react-app para iniciar un nuevo proyecto:
npx create-react-app my-app
cd my-app
yarn start
A continuación, abra una ventana de terminal separada e instale los componentes con estilo:
yarn add styled-components
Lo siguiente que debe hacer es crear dos archivos. El primero es global.js
, que contendrá nuestro estilo base, y el segundo es theme.js
, que incluirá variables para nuestros temas oscuros y claros:
// theme.js
export const lightTheme = {
body: '#E2E2E2',
text: '#363537',
toggleBorder: '#FFF',
gradient: 'linear-gradient(#39598A, #79D7ED)',
}
export const darkTheme = {
body: '#363537',
text: '#FAFAFA',
toggleBorder: '#6B8096',
gradient: 'linear-gradient(#091236, #1E215D)',
}
Siéntase libre de personalizar las variables de la forma que desee, ya que este código se usa solo con fines de demostración.
// global.js
// Source: https://github.com/maximakymenko/react-day-night-toggle-app/blob/master/src/global.js#L23-L41
import { createGlobalStyle } from 'styled-components';
export const GlobalStyles = createGlobalStyle`
*,
*::after,
*::before {
box-sizing: border-box;
}
body {
align-items: center;
background: ${({ theme }) => theme.body};
color: ${({ theme }) => theme.text};
display: flex;
flex-direction: column;
justify-content: center;
height: 100vh;
margin: 0;
padding: 0;
font-family: BlinkMacSystemFont, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
transition: all 0.25s linear;
}
Vaya al archivo App.js. Vamos a eliminar todo lo que hay allí y agregar el diseño de nuestra aplicación. Esto es lo que hice:
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { lightTheme, darkTheme } from './theme';
import { GlobalStyles } from './global';
function App() {
return (
<ThemeProvider theme={lightTheme}>
<>
<GlobalStyles />
<button>Toggle theme</button>
<h1>It's a light theme!</h1>
<footer>
</footer>
</>
</ThemeProvider>
);
}
export default App;
Esto importa nuestros temas claros y oscuros. El ThemeProvider
El componente también se importa y se le pasa el tema ligero (lightTheme
) estilos en el interior. También importamos GlobalStyles
para apretar todo en un solo lugar.
Esto es aproximadamente lo que tenemos hasta ahora:
Ahora, la funcionalidad de alternancia
Todavía no hay un cambio mágico entre temas, así que implementemos la funcionalidad de alternancia. Solo necesitaremos un par de líneas de código para que funcione.
Primero, importe el useState
gancho de react
:
// App.js
import React, { useState } from 'react';
A continuación, use el gancho para crear un estado local que realizará un seguimiento del tema actual y agregará una función para cambiar entre los temas al hacer clic:
// App.js
const [theme, setTheme] = useState('light');
// The function that toggles between themes
const toggleTheme = () => {
// if the theme is not light, then set it to dark
if (theme === 'light') {
setTheme('dark');
// otherwise, it should be light
} else {
setTheme('light');
}
}
Después de eso, todo lo que queda es pasar esta función a nuestro elemento de botón y cambiar condicionalmente el tema. Echar un vistazo:
// App.js
import React, { useState } from 'react';
import { ThemeProvider } from 'styled-components';
import { lightTheme, darkTheme } from './theme';
import { GlobalStyles } from './global';
// The function that toggles between themes
function App() {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
if (theme === 'light') {
setTheme('dark');
} else {
setTheme('light');
}
}
// Return the layout based on the current theme
return (
<ThemeProvider theme={theme === 'light' ? lightTheme : darkTheme}>
<>
<GlobalStyles />
// Pass the toggle functionality to the button
<button onClick={toggleTheme}>Toggle theme</button>
<h1>It's a light theme!</h1>
<footer>
</footer>
</>
</ThemeProvider>
);
}
export default App;
¿Como funciona?
// global.js
background: ${({ theme }) => theme.body};
color: ${({ theme }) => theme.text};
transition: all 0.25s linear;
Anteriormente en nuestro GlobalStyles
, asignamos background
y color
propiedades a valores de la theme
objeto, por lo que ahora, cada vez que cambiamos el conmutador, los valores cambian dependiendo de la darkTheme
y lightTheme
objetos a los que estamos pasando ThemeProvider
. El transition
La propiedad nos permite hacer este cambio un poco más suavemente que trabajar con animaciones de fotogramas clave.
Ahora necesitamos el componente de alternancia
Por lo general, terminamos aquí porque ahora sabe cómo crear funciones de alternancia. Sin embargo, siempre podemos hacerlo mejor, así que mejoremos la aplicación creando un Toggle
componente y hacer que nuestra funcionalidad de interruptor sea reutilizable. Ese es uno de los beneficios clave de hacer esto en React, ¿verdad?
Mantendremos todo dentro de un archivo por simplicidad, así que creemos uno nuevo llamado Toggle.js
y agregue lo siguiente:
// Toggle.js
import React from 'react'
import { func, string } from 'prop-types';
import styled from 'styled-components';
// Import a couple of SVG files we'll use in the design: https://www.flaticon.com
import { ReactComponent as MoonIcon } from 'icons/moon.svg';
import { ReactComponent as SunIcon } from 'icons/sun.svg';
const Toggle = ({ theme, toggleTheme }) => {
const isLight = theme === 'light';
return (
<button onClick={toggleTheme} >
<SunIcon />
<MoonIcon />
</button>
);
};
Toggle.propTypes = {
theme: string.isRequired,
toggleTheme: func.isRequired,
}
export default Toggle;
Puede descargar iconos desde aquí y aquí. Además, si queremos usar iconos como componentes, recuerde importarlos como componentes de React.
Pasamos dos puntales adentro: el theme
proporcionará el tema actual (claro u oscuro) y toggleTheme
La función se utilizará para cambiar entre ellos. A continuación creamos un isLight
variable, que devolverá un valor booleano dependiendo de nuestro tema actual. Lo pasaremos más tarde a nuestro componente con estilo.
También hemos importado un styled
función de los componentes con estilo, así que usémosla. Siéntase libre de agregar esto en la parte superior de su archivo después de las importaciones o crear un archivo dedicado para eso (por ejemplo, Toggle.styled.js) como lo tengo a continuación. Nuevamente, esto es puramente con fines de presentación, por lo que puede diseñar su componente como mejor le parezca.
// Toggle.styled.js
const ToggleContainer = styled.button`
background: ${({ theme }) => theme.gradient};
border: 2px solid ${({ theme }) => theme.toggleBorder};
border-radius: 30px;
cursor: pointer;
display: flex;
font-size: 0.5rem;
justify-content: space-between;
margin: 0 auto;
overflow: hidden;
padding: 0.5rem;
position: relative;
width: 8rem;
height: 4rem;
svg {
height: auto;
width: 2.5rem;
transition: all 0.3s linear;
// sun icon
&:first-child {
transform: ${({ lightTheme }) => lightTheme ? 'translateY(0)' : 'translateY(100px)'};
}
// moon icon
&:nth-child(2) {
transform: ${({ lightTheme }) => lightTheme ? 'translateY(-100px)' : 'translateY(0)'};
}
}
`;
Importar iconos como componentes nos permite cambiar directamente los estilos de los iconos SVG. Estamos comprobando si el lightTheme
es uno activo, y si es así, sacamos el ícono apropiado del área visible, algo así como la luna desapareciendo cuando es de día y viceversa.
No olvide reemplazar el botón con el ToggleContainer
componente en Toggle.js, independientemente de si está aplicando estilo en un archivo separado o directamente en Toggle.js. Asegúrese de pasar el isLight
variable para especificar el tema actual. Llamé a la utilería lightTheme
por lo que reflejaría claramente su propósito.
Lo último que debe hacer es importar nuestro componente dentro de App.js y pasarle los accesorios requeridos. Además, para agregar un poco más de interactividad, pasé la condición para alternar entre “claro” y “oscuro” en el encabezado cuando cambia el tema:
// App.js
<Toggle theme={theme} toggleTheme={toggleTheme} />
<h1>It's a {theme === 'light' ? 'light theme' : 'dark theme'}!</h1>
No olvide dar crédito a los autores de flaticon.com por proporcionar los iconos.
// App.js
<span>Credits:</span>
<small><b>Sun</b> icon made by <a href="https://www.flaticon.com/authors/smalllikeart">smalllikeart</a> from <a href="https://www.flaticon.com">www.flaticon.com</a></small>
<small><b>Moon</b> icon made by <a href="https://www.freepik.com/home">Freepik</a> from <a href="https://www.flaticon.com">www.flaticon.com</a></small>
Ahora eso es mejor:
El gancho useDarkMode
Al construir una aplicación, debemos tener en cuenta que la aplicación debe ser escalable, es decir, reutilizable, para que podamos usarla en muchos lugares o incluso en diferentes proyectos.
Es por eso que sería genial si moviéramos nuestra funcionalidad de alternancia a un lugar separado, entonces, ¿por qué no crear un gancho de cuenta dedicado para eso?
Creemos un nuevo archivo llamado useDarkMode.js en el proyecto src
directorio y mover nuestra lógica a este archivo con algunos ajustes:
// useDarkMode.js
import { useEffect, useState } from 'react';
export const useDarkMode = () => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
if (theme === 'light') {
window.localStorage.setItem('theme', 'dark')
setTheme('dark')
} else {
window.localStorage.setItem('theme', 'light')
setTheme('light')
}
};
useEffect(() => {
const localTheme = window.localStorage.getItem('theme');
localTheme && setTheme(localTheme);
}, []);
return [theme, toggleTheme]
};
Hemos agregado un par de cosas aquí. Queremos que nuestro tema persista entre sesiones en el navegador, por lo que si alguien ha elegido un tema oscuro, eso es lo que obtendrá en la próxima visita a la aplicación. Esa es una gran mejora de UX. Por esta razón utilizamos localStorage
.
También hemos implementado el useEffect
gancho para comprobar el montaje de los componentes. Si el usuario ha seleccionado previamente un tema, lo pasaremos a nuestro setTheme
función. Al final, devolveremos nuestro theme
, que contiene el elegido theme
y toggleTheme
función para cambiar entre modos.
Ahora, implementemos el useDarkMode
gancho. Vaya a App.js, importe el gancho recién creado, desestructura nuestro theme
y toggleTheme
propiedades del gancho y colóquelas donde pertenecen:
// App.js
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { useDarkMode } from './useDarkMode';
import { lightTheme, darkTheme } from './theme';
import { GlobalStyles } from './global';
import Toggle from './components/Toggle';
function App() {
const [theme, toggleTheme] = useDarkMode();
const themeMode = theme === 'light' ? lightTheme : darkTheme;
return (
<ThemeProvider theme={themeMode}>
<>
<GlobalStyles />
<Toggle theme={theme} toggleTheme={toggleTheme} />
<h1>It's a {theme === 'light' ? 'light theme' : 'dark theme'}!</h1>
<footer>
Credits:
<small>Sun icon made by smalllikeart from www.flaticon.com</small>
<small>Moon icon made by Freepik from www.flaticon.com</small>
</footer>
</>
</ThemeProvider>
);
}
export default App;
Esto funciona casi a la perfección, pero hay una pequeña cosa que podemos hacer para mejorar nuestra experiencia. Cambie al tema oscuro y vuelva a cargar la página. ¿Ves que el ícono del sol se carga antes que el ícono de la luna por un breve momento?
Eso pasa porque nuestro useState
gancho inicia el light
tema inicialmente. Después, useEffect
corre, comprueba localStorage
y solo entonces establece el theme
a dark
.
Hasta ahora, encontré dos soluciones. La primera es comprobar si hay un valor en localStorage
en nuestro useState
:
// useDarkMode.js
const [theme, setTheme] = useState(window.localStorage.getItem('theme') || 'light');
Sin embargo, no estoy seguro de si es una buena práctica realizar comprobaciones como esa en el interior useState
, déjame mostrarte una segunda solución que estoy usando.
Este será un poco más complicado. Crearemos otro estado y lo llamaremos componentMounted
. Entonces, dentro del useEffect
gancho, donde comprobamos nuestro localTheme
, agregaremos un else
declaración, y si no hay theme
en localStorage
, lo agregaremos. Después de eso, estableceremos setComponentMounted
a true
. Al final, agregamos componentMounted
a nuestra declaración de devolución.
// useDarkMode.js
import { useEffect, useState } from 'react';
export const useDarkMode = () => {
const [theme, setTheme] = useState('light');
const [componentMounted, setComponentMounted] = useState(false);
const toggleTheme = () => {
if (theme === 'light') {
window.localStorage.setItem('theme', 'dark');
setTheme('dark');
} else {
window.localStorage.setItem('theme', 'light');
setTheme('light');
}
};
useEffect(() => {
const localTheme = window.localStorage.getItem('theme');
if (localTheme) {
setTheme(localTheme);
} else {
setTheme('light')
window.localStorage.setItem('theme', 'light')
}
setComponentMounted(true);
}, []);
return [theme, toggleTheme, componentMounted]
};
Es posible que haya notado que tenemos algunos fragmentos de código que se repiten. Siempre tratamos de seguir el principio DRY mientras escribimos el código, y aquí mismo tenemos la oportunidad de usarlo. Podemos crear una función separada que establecerá nuestro estado y pasará theme
al localStorage
. Creo que el mejor nombre para ello será setTheme
, pero ya lo usamos, así que llamémoslo setMode
:
// useDarkMode.js
const setMode = mode => {
window.localStorage.setItem('theme', mode)
setTheme(mode)
};
Con esta función en su lugar, podemos refactorizar un poco nuestro useDarkMode.js:
// useDarkMode.js
import { useEffect, useState } from 'react';
export const useDarkMode = () => {
const [theme, setTheme] = useState('light');
const [componentMounted, setComponentMounted] = useState(false);
const setMode = mode => {
window.localStorage.setItem('theme', mode)
setTheme(mode)
};
const toggleTheme = () => {
if (theme === 'light') {
setMode('dark');
} else {
setMode('light');
}
};
useEffect(() => {
const localTheme = window.localStorage.getItem('theme');
if (localTheme) {
setTheme(localTheme);
} else {
setMode('light');
}
setComponentMounted(true);
}, []);
return [theme, toggleTheme, componentMounted]
};
Solo hemos cambiado un poco el código, ¡pero se ve mucho mejor y es más fácil de leer y comprender!
¿Se montó el componente?
Volviendo a componentMounted
propiedad. Lo usaremos para comprobar si nuestro componente se ha montado porque esto es lo que sucede en useEffect
gancho.
Si aún no ha sucedido, renderizaremos un div vacío:
// App.js
if (!componentMounted) {
return <div />
};
Así es como se completa el código para App.js:
// App.js
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { useDarkMode } from './useDarkMode';
import { lightTheme, darkTheme } from './theme';
import { GlobalStyles } from './global';
import Toggle from './components/Toggle';
function App() {
const [theme, toggleTheme, componentMounted] = useDarkMode();
const themeMode = theme === 'light' ? lightTheme : darkTheme;
if (!componentMounted) {
return <div />
};
return (
<ThemeProvider theme={themeMode}>
<>
<GlobalStyles />
<Toggle theme={theme} toggleTheme={toggleTheme} />
<h1>It's a {theme === 'light' ? 'light theme' : 'dark theme'}!</h1>
<footer>
<span>Credits:</span>
<small><b>Sun</b> icon made by <a href="https://www.flaticon.com/authors/smalllikeart">smalllikeart</a> from <a href="https://www.flaticon.com">www.flaticon.com</a></small>
<small><b>Moon</b> icon made by <a href="https://www.freepik.com/home">Freepik</a> from <a href="https://www.flaticon.com">www.flaticon.com</a></small>
</footer>
</>
</ThemeProvider>
);
}
export default App;
Usando el esquema de color preferido del usuario
Esta parte no es necesaria, pero le permitirá lograr una experiencia de usuario aún mejor. Esta función multimedia se utiliza para detectar si el usuario ha solicitado que la página utilice un tema de color claro u oscuro según la configuración de su sistema operativo. Por ejemplo, si el esquema de color predeterminado de un usuario en un teléfono o computadora portátil está configurado en oscuro, su sitio web cambiará su esquema de color en consecuencia. Vale la pena señalar que esta consulta de medios todavía es un trabajo en progreso y está incluida en la especificación de nivel 5 de consultas de medios, que se encuentra en el Borrador del editor.
Estos datos de soporte del navegador son de Caniuse, que tiene más detalles. Un número indica que el navegador admite la función en esa versión y posteriores.
Escritorio
Cromo | Firefox | ES DECIR | Borde | Safari |
---|---|---|---|---|
76 | 67 | No | 79 | 12,1 |
Móvil / Tableta
Android Chrome | Android Firefox | Androide | Safari de iOS |
---|---|---|---|
96 | 94 | 96 | 13.0-13.1 |
La implementación es bastante sencilla. Debido a que estamos trabajando con una consulta de medios, debemos verificar si el navegador lo admite en el useEffect
enganche y establezca el tema apropiado. Para hacer eso, usaremos window.matchMedia
para comprobar si existe y si se admite el modo oscuro. También debemos recordar acerca de la localTheme
porque, si está disponible, no queremos sobrescribirlo con el valor oscuro a menos que, por supuesto, el valor esté establecido en claro.
Si se pasan todas las comprobaciones, configuraremos el tema oscuro.
// useDarkMode.js
useEffect(() => {
if (
window.matchMedia &&
window.matchMedia('(prefers-color-scheme: dark)').matches &&
!localTheme
) {
setTheme('dark')
}
})
Como se mencionó anteriormente, debemos recordar la existencia de localTheme
– es por eso que necesitamos implementar nuestra lógica anterior donde la hemos verificado.
Esto es lo que teníamos antes:
// useDarkMode.js
useEffect(() => {
const localTheme = window.localStorage.getItem('theme');
if (localTheme) {
setTheme(localTheme);
} else {
setMode('light');
}
})
Vamos a mezclarlo. Reemplacé las declaraciones if y else con operadores ternarios para hacer las cosas un poco más legibles también:
// useDarkMode.js
useEffect(() => {
const localTheme = window.localStorage.getItem('theme');
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && !localTheme ?
setMode('dark') :
localTheme ?
setTheme(localTheme) :
setMode('light');})
})
Aquí está el archivo userDarkMode.js con el código completo:
// useDarkMode.js
import { useEffect, useState } from 'react';
export const useDarkMode = () => {
const [theme, setTheme] = useState('light');
const [componentMounted, setComponentMounted] = useState(false);
const setMode = mode => {
window.localStorage.setItem('theme', mode)
setTheme(mode)
};
const toggleTheme = () => {
if (theme === 'light') {
setMode('dark')
} else {
setMode('light')
}
};
useEffect(() => {
const localTheme = window.localStorage.getItem('theme');
window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches && !localTheme ?
setMode('dark') :
localTheme ?
setTheme(localTheme) :
setMode('light');
setComponentMounted(true);
}, []);
return [theme, toggleTheme, componentMounted]
};
¡Darle una oportunidad! Cambia el modo, persiste el tema en localStorage
, y también establece el tema predeterminado de acuerdo con el esquema de color del sistema operativo, si está disponible.
¡Felicidades mi amigo! ¡Gran trabajo! Si tiene alguna pregunta sobre la implementación, no dude en envíeme un mensaje!