Skip to main content

Modo Oscuro

Agrega soporte de modo oscuro a tu aplicación con transiciones suaves y detección de preferencias del sistema.

La librería nach-themes proporciona una forma simple y elegante de implementar modo oscuro en tus aplicaciones React, con soporte para preferencias del sistema, transiciones suaves y animaciones al hacer clic.

Instalación

Instala el paquete vía pnpm:
npm install install nach-themes

Inicio Rápido

1. Configura tu CSS

Primero, asegúrate de tener definidas las variables de tema tanto para modo claro como oscuro en tu CSS:
1:root {
2 --background: oklch(99.405% 0.00011 271.152);
3 --foreground: oklch(0% 0 0);
4 /* ... otras variables de modo claro */
5}
6
7.dark {
8 --background: oklch(20% 0.02 230);
9 --foreground: oklch(96% 0.008 230);
10 /* ... otras variables de modo oscuro */
11}

2. Envuelve tu app con ThemeProvider

Crea un componente providers para envolver tu aplicación:
1'use client';
2
3import { ThemeProvider } from 'nach-themes';
4
5export function Providers({ children }: { children: React.ReactNode }) {
6 return <ThemeProvider>{children}</ThemeProvider>;
7}
Luego úsalo en tu layout raíz:
1// app/layout.tsx
2import { Providers } from '@/components/providers';
3
4export default function RootLayout({ children }: { children: React.ReactNode }) {
5 return (
6 <html lang="es" suppressHydrationWarning>
7 <body>
8 <Providers>{children}</Providers>
9 </body>
10 </html>
11 );
12}

3. Crea un botón de cambio de tema

1'use client';
2
3import { useTheme } from 'nach-themes';
4import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
5import { Button } from '@/components/ui/button';
6
7export function ThemeToggle() {
8 const { theme, setTheme } = useTheme();
9
10 const isDark = theme === 'dark';
11 const Icon = isDark ? MoonIcon : SunIcon;
12
13 return (
14 <Button
15 variant="ghost"
16 size="icon"
17 onClick={(e) => setTheme(isDark ? 'light' : 'dark', e)}
18 aria-label={`Cambiar a modo ${isDark ? 'claro' : 'oscuro'}`}
19 >
20 <Icon className="h-5 w-5" />
21 </Button>
22 );
23}

Características

Detección de Tema del Sistema

Por defecto, el provider respeta la preferencia del sistema del usuario:
1<ThemeProvider>{children}</ThemeProvider>
La librería detecta automáticamente cambios en las preferencias del sistema y actualiza el tema en consecuencia cuando está configurado en 'system'.

Temas Disponibles

El hook useTheme proporciona acceso a tres modos de tema:
  • 'light' - Modo claro
  • 'dark' - Modo oscuro
  • 'system' - Sigue la preferencia del sistema
1const { theme, setTheme, themes } = useTheme();
2
3// themes = ['light', 'dark', 'system']

Transiciones Suaves con View Transitions

La librería incluye soporte integrado para la View Transitions API, creando transiciones animadas suaves entre temas. Cuando pasas un evento de clic a setTheme, crea una animación circular de revelación desde la posición del clic:
1<button onClick={(e) => setTheme('dark', e)}>Cambiar Tema</button>
Sin el evento de clic, recurre a una transición estándar de fundido cruzado:
1<button onClick={() => setTheme('dark')}>Cambiar Tema</button>

Tema Resuelto

Obtén el tema real que se está aplicando, incluso cuando está configurado en 'system':
1const { theme, resolvedTheme } = useTheme();
2
3// theme = 'system'
4// resolvedTheme = 'dark' (si el sistema prefiere oscuro)

Uso Avanzado

Toggle de Tema con Estado de Carga

Maneja el estado de hidratación para prevenir cambios en el layout:
1'use client';
2
3import { useMounted } from '@/hooks/use-mounted';
4import { useTheme } from 'nach-themes';
5import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
6import { Button } from '@/components/ui/button';
7
8export function ThemeToggle() {
9 const mounted = useMounted();
10 const { theme, setTheme } = useTheme();
11
12 if (!mounted) {
13 return (
14 <Button
15 variant="ghost"
16 size="icon"
17 disabled
18 className="bg-muted/30 animate-pulse cursor-default"
19 >
20 <div className="bg-foreground/20 h-5 w-5 rounded-full" />
21 </Button>
22 );
23 }
24
25 const isDark = theme === 'dark';
26 const Icon = isDark ? MoonIcon : SunIcon;
27
28 return (
29 <Button
30 variant="ghost"
31 size="icon"
32 onClick={(e) => setTheme(isDark ? 'light' : 'dark', e)}
33 aria-label={`Cambiar a modo ${isDark ? 'claro' : 'oscuro'}`}
34 className="h-8 w-8"
35 >
36 <Icon className="h-5 w-5" />
37 </Button>
38 );
39}
El hook useMounted puede implementarse como:
1import { useEffect, useState } from 'react';
2
3export function useMounted() {
4 const [mounted, setMounted] = useState(false);
5
6 useEffect(() => {
7 setMounted(true);
8 }, []);
9
10 return mounted;
11}

Menú Desplegable de Tema

Crea un selector de tema más sofisticado:
1'use client';
2
3import { useTheme } from 'nach-themes';
4import {
5 DropdownMenu,
6 DropdownMenuContent,
7 DropdownMenuItem,
8 DropdownMenuTrigger,
9} from '@/components/ui/dropdown-menu';
10import { Button } from '@/components/ui/button';
11import { MoonIcon, SunIcon, DesktopIcon } from '@radix-ui/react-icons';
12
13export function ThemeDropdown() {
14 const { theme, setTheme } = useTheme();
15
16 return (
17 <DropdownMenu>
18 <DropdownMenuTrigger asChild>
19 <Button variant="ghost" size="icon">
20 <SunIcon className="h-5 w-5 scale-100 rotate-0 transition-transform dark:scale-0 dark:-rotate-90" />
21 <MoonIcon className="absolute h-5 w-5 scale-0 rotate-90 transition-transform dark:scale-100 dark:rotate-0" />
22 <span className="sr-only">Cambiar tema</span>
23 </Button>
24 </DropdownMenuTrigger>
25 <DropdownMenuContent align="end">
26 <DropdownMenuItem onClick={() => setTheme('light')}>
27 <SunIcon className="mr-2 h-4 w-4" />
28 Claro
29 </DropdownMenuItem>
30 <DropdownMenuItem onClick={() => setTheme('dark')}>
31 <MoonIcon className="mr-2 h-4 w-4" />
32 Oscuro
33 </DropdownMenuItem>
34 <DropdownMenuItem onClick={() => setTheme('system')}>
35 <DesktopIcon className="mr-2 h-4 w-4" />
36 Sistema
37 </DropdownMenuItem>
38 </DropdownMenuContent>
39 </DropdownMenu>
40 );
41}

Cambios de Tema Programáticos

Accede a información del tema en cualquier parte de tu app:
1'use client';
2
3import { useTheme } from 'nach-themes';
4import { useEffect } from 'react';
5
6export function ContenidoDinamico() {
7 const { resolvedTheme } = useTheme();
8
9 useEffect(() => {
10 // Actualiza librerías de terceros según el tema
11 if (resolvedTheme === 'dark') {
12 // Inicializa modo oscuro para servicios externos
13 }
14 }, [resolvedTheme]);
15
16 return (
17 <div>
18 <p>Tema actual: {resolvedTheme}</p>
19 </div>
20 );
21}

Soporte de TypeScript

La librería está completamente tipada. Todos los hooks y componentes tienen definiciones completas de TypeScript:
1import { Theme } from 'nach-themes';
2
3const miTema: Theme = 'dark';