Interruptor de tema sol-luna
El sol se convierte en luna recortada, los rayos se retraen y aparecen estrellas; todo en un SVG animado con CSS.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useState, type CSSProperties } from "react";
/** Interruptor de tema claro/oscuro: el sol se convierte en luna recortada, los rayos se retraen y aparecen estrellas. Un solo SVG animado con CSS. React + TypeScript, sin dependencias. */
export type InterruptorTemaProps = {
dark?: boolean;
defaultDark?: boolean;
onChange?: (dark: boolean) => void;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcmit-root { box-sizing: border-box; display: inline-block; }
.gcmit-root *, .gcmit-root *::before, .gcmit-root *::after { box-sizing: border-box; }
.gcmit-root :focus-visible { outline: 3px solid #5b8def; outline-offset: 4px; border-radius: 999px; }
@media (prefers-reduced-motion: reduce) { .gcmit-root * { transition-duration: .01ms !important; } }
.gcmit-btn { position: relative; width: 96px; height: 48px; padding: 0; border: 0; border-radius: 999px; cursor: pointer; overflow: hidden; background: linear-gradient(180deg, #8fd0ff, #cdeaff); box-shadow: inset 0 2px 6px rgba(0, 40, 90, .3), 0 1px 0 rgba(255, 255, 255, .8); transition: background .6s; -webkit-tap-highlight-color: transparent; }
.gcmit-btn[aria-checked="true"] { background: linear-gradient(180deg, #121a3a, #2a3566); }
.gcmit-stars { position: absolute; inset: 0; opacity: 0; transition: opacity .5s; }
.gcmit-btn[aria-checked="true"] .gcmit-stars { opacity: 1; }
.gcmit-stars i { position: absolute; width: 3px; height: 3px; border-radius: 50%; background: #fff; }
.gcmit-orb { position: absolute; top: 5px; left: 5px; width: 38px; height: 38px; transition: transform .6s cubic-bezier(.65, 0, .35, 1); }
.gcmit-btn[aria-checked="true"] .gcmit-orb { transform: translateX(48px); }
.gcmit-orb svg { width: 100%; height: 100%; overflow: visible; }
.gcmit-sun { fill: #ffd23f; stroke: #ffa800; stroke-width: 1; transition: fill .6s; }
.gcmit-btn[aria-checked="true"] .gcmit-sun { fill: #f1eee0; stroke: #cfcab5; }
.gcmit-rays { transform-origin: 50% 50%; transition: transform .6s, opacity .4s; stroke: #ffc400; stroke-width: 2; stroke-linecap: round; }
.gcmit-btn[aria-checked="true"] .gcmit-rays { transform: rotate(90deg) scale(.4); opacity: 0; }
.gcmit-bite { fill: #2a3566; transform: translate(18px, -8px); opacity: 0; transition: transform .6s cubic-bezier(.65, 0, .35, 1), opacity .3s; }
.gcmit-btn[aria-checked="true"] .gcmit-bite { transform: translate(0, 0); opacity: 1; }
.gcmit-cloud { position: absolute; left: 12px; bottom: 6px; width: 34px; height: 12px; border-radius: 99px; background: #fff; box-shadow: 10px -6px 0 -1px #fff, 20px 0 0 -1px #fff; transition: transform .6s, opacity .5s; }
.gcmit-btn[aria-checked="true"] .gcmit-cloud { transform: translateY(30px); opacity: 0; }`;
export function InterruptorTema({ dark, defaultDark = false, onChange, className = "", style }: InterruptorTemaProps) {
const [inner, setInner] = useState(defaultDark);
const on = dark ?? inner;
return (
<span className={`gcmit-root ${className}`} style={style}>
<style>{CSS}</style>
<button className="gcmit-btn" type="button" role="switch" aria-checked={on} aria-label="Tema oscuro" onClick={() => { setInner(!on); onChange?.(!on); }}>
<span className="gcmit-cloud" aria-hidden="true" />
<span className="gcmit-stars" aria-hidden="true">
<i style={{ left: 14, top: 10 }} />
<i style={{ left: 26, top: 28 }} />
<i style={{ left: 40, top: 14, width: 2, height: 2 }} />
<i style={{ left: 8, top: 34, width: 2, height: 2 }} />
</span>
<span className="gcmit-orb" aria-hidden="true">
<svg viewBox="0 0 40 40">
<g className="gcmit-rays">
{Array.from({ length: 8 }, (_, i) => (
<line key={i} x1="20" y1="2" x2="20" y2="6" transform={`rotate(${i * 45} 20 20)`} />
))}
</g>
<circle className="gcmit-sun" cx="20" cy="20" r="11" />
<circle className="gcmit-bite" cx="25" cy="16" r="9" />
</svg>
</span>
</button>
</span>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/interruptor-temaEjemplo de uso de Interruptor de tema sol-luna
Después de copiar interruptor-tema.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { InterruptorTema } from "./interruptor-tema";
export default function Ejemplo() {
return <InterruptorTema dark />;
}Propiedades de InterruptorTema
InterruptorTema acepta 5 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
dark? | boolean | Opcional. |
defaultDark? | boolean | Opcional. |
onChange? | (dark: boolean) => void | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Accesibilidad y rendimiento
El archivo tiene 65 líneas (4 KB) y no necesita @keyframes. Esto es lo que hace Interruptor de tema sol-luna por quien lo usa:
- Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
- Muestra un anillo de foco visible al navegar con teclado (:focus-visible).
- Declara roles ARIA para lectores de pantalla: switch.
- Es un componente de cliente de Next.js: incluye la directiva "use client".