Modo Oscuro

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

La librería i7a-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:

pnpm add i7a-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:

:root {
--background: oklch(99.405% 0.00011 271.152);
--foreground: oklch(0% 0 0);
/* ... otras variables de modo claro */
}
.dark {
--background: oklch(20% 0.02 230);
--foreground: oklch(96% 0.008 230);
/* ... otras variables de modo oscuro */
}

2. Envuelve tu app con ThemeProvider

Crea un componente providers para envolver tu aplicación:

'use client';
import { ThemeProvider } from 'i7a-themes';
export function Providers({ children }: { children: React.ReactNode }) {
return <ThemeProvider>{children}</ThemeProvider>;
}

Luego úsalo en tu layout raíz:

// app/layout.tsx
import { Providers } from '@/components/providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="es" suppressHydrationWarning>
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}

3. Crea un botón de cambio de tema

'use client';
import { useTheme } from 'i7a-themes';
import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const { theme, setTheme } = useTheme();
const isDark = theme === 'dark';
const Icon = isDark ? MoonIcon : SunIcon;
return (
<Button
variant="ghost"
size="icon"
onClick={(e) => setTheme(isDark ? 'light' : 'dark', e)}
aria-label={`Cambiar a modo ${isDark ? 'claro' : 'oscuro'}`}
>
<Icon className="h-5 w-5" />
</Button>
);
}

Características

Detección de Tema del Sistema

Por defecto, el provider respeta la preferencia del sistema del usuario:

<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
const { theme, setTheme, themes } = useTheme();
// 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:

<button onClick={(e) => setTheme('dark', e)}>Cambiar Tema</button>

Sin el evento de clic, recurre a una transición estándar de fundido cruzado:

<button onClick={() => setTheme('dark')}>Cambiar Tema</button>

Tema Resuelto

Obtén el tema real que se está aplicando, incluso cuando está configurado en 'system':

const { theme, resolvedTheme } = useTheme();
// theme = 'system'
// 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:

'use client';
import { useMounted } from '@/hooks/use-mounted';
import { useTheme } from 'i7a-themes';
import { MoonIcon, SunIcon } from '@radix-ui/react-icons';
import { Button } from '@/components/ui/button';
export function ThemeToggle() {
const mounted = useMounted();
const { theme, setTheme } = useTheme();
if (!mounted) {
return (
<Button
variant="ghost"
size="icon"
disabled
className="bg-muted/30 animate-pulse cursor-default"
>
<div className="bg-foreground/20 h-5 w-5 rounded-full" />
</Button>
);
}
const isDark = theme === 'dark';
const Icon = isDark ? MoonIcon : SunIcon;
return (
<Button
variant="ghost"
size="icon"
onClick={(e) => setTheme(isDark ? 'light' : 'dark', e)}
aria-label={`Cambiar a modo ${isDark ? 'claro' : 'oscuro'}`}
className="h-8 w-8"
>
<Icon className="h-5 w-5" />
</Button>
);
}

El hook useMounted puede implementarse como:

import { useEffect, useState } from 'react';
export function useMounted() {
const [mounted, setMounted] = useState(false);
useEffect(() => {
setMounted(true);
}, []);
return mounted;
}

Menú Desplegable de Tema

Crea un selector de tema más sofisticado:

'use client';
import { useTheme } from 'i7a-themes';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Button } from '@/components/ui/button';
import { MoonIcon, SunIcon, DesktopIcon } from '@radix-ui/react-icons';
export function ThemeDropdown() {
const { theme, setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="icon">
<SunIcon className="h-5 w-5 scale-100 rotate-0 transition-transform dark:scale-0 dark:-rotate-90" />
<MoonIcon className="absolute h-5 w-5 scale-0 rotate-90 transition-transform dark:scale-100 dark:rotate-0" />
<span className="sr-only">Cambiar tema</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => setTheme('light')}>
<SunIcon className="mr-2 h-4 w-4" />
Claro
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<MoonIcon className="mr-2 h-4 w-4" />
Oscuro
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<DesktopIcon className="mr-2 h-4 w-4" />
Sistema
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}

Cambios de Tema Programáticos

Accede a información del tema en cualquier parte de tu app:

'use client';
import { useTheme } from 'i7a-themes';
import { useEffect } from 'react';
export function ContenidoDinamico() {
const { resolvedTheme } = useTheme();
useEffect(() => {
// Actualiza librerías de terceros según el tema
if (resolvedTheme === 'dark') {
// Inicializa modo oscuro para servicios externos
}
}, [resolvedTheme]);
return (
<div>
<p>Tema actual: {resolvedTheme}</p>
</div>
);
}

Soporte de TypeScript

La librería está completamente tipada. Todos los hooks y componentes tienen definiciones completas de TypeScript:

import { Theme } from 'i7a-themes';
const miTema: Theme = 'dark';
Modo Oscuro - I7A UI