Ir al contenido
Evaluar mi proyecto
Componentes · MicrointeraccionesDisponible

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.

interruptor-tema.tsx65 líneas
Descargar
"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.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/interruptor-tema

Ejemplo 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.

ejemplo.tsx5 líneas
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.

PropiedadTipoDescripción
dark?booleanOpcional.
defaultDark?booleanOpcional.
onChange?(dark: boolean) => voidOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)