Fondo de cintas cinéticas
Cintas diagonales de color con ritmo lento, pausa manual y suspensión automática al salir de pantalla.
El ritmo de una idea.
Cintas de color, movimiento contenido.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import {
useState,
useEffect,
useRef,
type CSSProperties,
type ReactNode,
} from "react";
/** Fondo de cintas cinéticas. React + TypeScript, sin dependencias. Personaliza con variables --gczr-*. */
export type FondoCintasProps = {
children?: ReactNode;
className?: string;
style?: CSSProperties;
};
const CSS = `
.gczr-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gczr-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gczr-root *, .gczr-root *::before, .gczr-root *::after { box-sizing: border-box; }
.gczr-root button, .gczr-root input, .gczr-root textarea, .gczr-root select { font: inherit; }
.gczr-root button, .gczr-root a { -webkit-tap-highlight-color: transparent; }
.gczr-root :focus-visible { outline: 2px solid var(--gczr-accent, #25724b); outline-offset: 4px; }
.gczr-root ::selection { background: var(--gczr-accent, #25724b); color: #fff; }
.gczr-root button:disabled { opacity: .5; cursor: not-allowed; }
.gczr-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gczr-root *, .gczr-root *::before, .gczr-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gczr-root { position:relative; isolation:isolate; overflow:hidden; display:grid; place-items:center; width:100%; min-height:400px; padding:48px 24px; background:var(--gczr-bg,#241c2d); color:var(--gczr-fg,#f3eaf7); }
.gczr-canvas { position:absolute; inset:-80% -20%; z-index:-1; transform:rotate(-25deg); display:flex; justify-content:center; gap:24px; opacity:.5; }
.gczr-canvas i { width:90px; background:linear-gradient(180deg,transparent,#a084b8 40%,#c78f8b 60%,transparent); transform:translateY(calc(var(--gczr-i) * 15px)); animation:gczr-shift 12s cubic-bezier(.45,0,.55,1) infinite alternate; animation-delay:calc(var(--gczr-i) * -1.3s); animation-play-state:paused; }
.gczr-root[data-active="true"] .gczr-canvas i { animation-play-state:running; }
.gczr-content { text-align:center; }
.gczr-content h3 { font-size:clamp(32px,5vw,52px); line-height:1.1; font-weight:600; letter-spacing:-.03em; margin:0 0 16px; }
.gczr-content p { font-size:14px; color:#e2cfe8; margin:0; }
.gczr-root>button { position:absolute; bottom:16px; right:18px; border:1px solid #9a83a4; border-radius:4px; min-height:44px; padding:10px 18px; background:#241c2d; color:#f3eaf7; cursor:pointer; font-size:11px; }
@keyframes gczr-shift { from { transform:translateY(-8%); } to { transform:translateY(8%); } }`;
function useMotionActive() {
const ref = useRef<HTMLDivElement>(null);
const [active, setActive] = useState(false);
useEffect(() => {
const media = window.matchMedia("(prefers-reduced-motion: reduce)");
let visible = false;
const sync = () => setActive(visible && !document.hidden && !media.matches);
const observer = new IntersectionObserver(([entry]) => {
visible = entry.isIntersecting;
sync();
});
if (ref.current) observer.observe(ref.current);
media.addEventListener("change", sync);
document.addEventListener("visibilitychange", sync);
return () => {
observer.disconnect();
media.removeEventListener("change", sync);
document.removeEventListener("visibilitychange", sync);
};
}, []);
return { ref, active };
}
export function FondoCintas({
children,
className = "",
style,
}: FondoCintasProps) {
const { ref, active } = useMotionActive();
const [paused, setPaused] = useState(false);
return (
<div
ref={ref}
className={`gczr-root ${className}`}
style={style}
data-active={active && !paused}
>
<style>{CSS}</style>
<div className="gczr-canvas" aria-hidden="true">
{Array.from({ length: 7 }, (_, i) => (
<i key={i} style={{ "--gczr-i": i } as CSSProperties} />
))}
</div>
<div className="gczr-content">
{children ?? (
<>
<h3>El ritmo de una idea.</h3>
<p>Cintas de color, movimiento contenido.</p>
</>
)}
</div>
<button
type="button"
aria-pressed={paused}
onClick={() => setPaused(!paused)}
>
{paused ? "Reanudar" : "Pausar"}
</button>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/fondo-cintasEjemplo de uso de Fondo de cintas cinéticas
Después de copiar fondo-cintas.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { FondoCintas } from "./fondo-cintas";
export default function Ejemplo() {
return <FondoCintas>Contenido</FondoCintas>;
}Propiedades de FondoCintas
FondoCintas acepta 3 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
children? | ReactNode | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Fondo de cintas cinéticas con variables CSS
Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.
| Variable | Valor por defecto |
|---|---|
--gczr-fg | #202421 |
--gczr-accent | #25724b |
--gczr-bg | #241c2d |
Accesibilidad y rendimiento
El archivo tiene 99 líneas (4 KB) y define 1 animación con @keyframes. Esto es lo que hace Fondo de cintas cinéticas 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).
- Expone su estado a tecnologías de apoyo con atributos aria-*.
- Se pausa cuando sale de la pantalla para no gastar batería ni CPU.
- Es un componente de cliente de Next.js: incluye la directiva "use client".