Hero de órbita interactiva
Hero verde profundo con conexiones orbitales SVG, nodos y trazo animado que se pausa fuera de pantalla y con movimiento reducido.
Cada idea encuentra su órbita.
Un lugar para conectar personas, proyectos y posibilidades.
Explorar el sistemaTodo conectado.IdeasEquipoDiseñoProducto
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 } from "react";
/** Hero de órbita interactiva. React + TypeScript, sin dependencias. Personaliza con variables --gcyo-*. */
export type HeroOrbitaProps = {
title?: string;
description?: string;
label?: string;
href?: string;
nodes?: string[];
className?: string;
style?: CSSProperties;
};
const CSS = `
.gcyo-root { box-sizing: border-box; min-width: 0; max-width: 100%; color: var(--gcyo-fg, #202421); font: 400 15px/1.5 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
.gcyo-root *, .gcyo-root *::before, .gcyo-root *::after { box-sizing: border-box; }
.gcyo-root button, .gcyo-root input, .gcyo-root textarea, .gcyo-root select { font: inherit; }
.gcyo-root button, .gcyo-root a { -webkit-tap-highlight-color: transparent; }
.gcyo-root :focus-visible { outline: 2px solid var(--gcyo-accent, #25724b); outline-offset: 4px; }
.gcyo-root ::selection { background: var(--gcyo-accent, #25724b); color: #fff; }
.gcyo-root button:disabled { opacity: .5; cursor: not-allowed; }
.gcyo-root svg { flex-shrink: 0; }
@media (prefers-reduced-motion: reduce) { .gcyo-root *, .gcyo-root *::before, .gcyo-root *::after { animation: none !important; transition-duration: .01ms !important; scroll-behavior: auto !important; } }
.gcyo-root { container-type:inline-size; width:100%; min-height:560px; background:var(--gcyo-bg,#10291f); color:var(--gcyo-fg,#f1f1dd); padding:50px clamp(24px,5vw,70px) 28px; overflow:hidden; }
.gcyo-section { display:grid; grid-template-columns:1.1fr 1fr; gap:40px; align-items:center; min-height:440px; }
.gcyo-copy h2 { font-size:clamp(42px,5vw,72px); font-weight:600; letter-spacing:-.035em; line-height:1.03; max-width:12ch; text-wrap:balance; margin:0 0 24px; }
.gcyo-copy p { color:#c3d0bc; font-size:16px; max-width:36ch; margin:0 0 30px; }
.gcyo-copy a { display:inline-flex; align-items:center; gap:22px; min-height:48px; background:#e5edce; color:#203d2d; padding:12px 22px; border-radius:4px; font-size:13px; font-weight:600; text-decoration:none; }
.gcyo-copy a:hover { background:#cfddb0; }
.gcyo-system { position:relative; aspect-ratio:1; min-width:0; width:100%; }
.gcyo-rings { width:100%; height:100%; stroke:#65876d; stroke-width:1; }
.gcyo-tracer { stroke:#d9e8b0; stroke-width:2; stroke-dasharray:28 947; transform-origin:center; animation:gcyo-orbit 18s linear infinite; animation-play-state:paused; }
.gcyo-root[data-active="true"] .gcyo-tracer { animation-play-state:running; }
.gcyo-center { position:absolute; inset:36%; display:grid; place-content:center; text-align:center; border:1px solid #9bac88; border-radius:50%; background:#173d2b; font-size:13px; line-height:1.6; }
.gcyo-node { position:absolute; top:calc(9% + var(--gcyo-i) * 23%); left:12%; border:1px solid #6c856c; background:#244730; color:#d7e1c8; padding:6px 14px; border-radius:20px; font-size:12px; transition:transform .4s cubic-bezier(.16,1,.3,1); }
.gcyo-node:nth-of-type(odd) { left:auto; right:3%; }
.gcyo-system:hover .gcyo-node { transform:translateY(-5px); }
.gcyo-bottom { display:flex; justify-content:space-between; gap:20px; color:#b6c7ae; font-size:11px; padding-top:24px; }
@keyframes gcyo-orbit { to { transform:rotate(360deg); } }
@container(max-width:620px) { .gcyo-section { grid-template-columns:1fr; gap:20px; }
.gcyo-copy h2 { font-size:44px; max-width:14ch; }
.gcyo-system { max-width:360px; justify-self:center; }
.gcyo-bottom { flex-direction:column; gap:8px; } }`;
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 HeroOrbita({
title = "Cada idea encuentra su órbita.",
description = "Un lugar para conectar personas, proyectos y posibilidades.",
label = "Explorar el sistema",
href = "#explorar",
nodes = ["Ideas", "Equipo", "Diseño", "Producto"],
className = "",
style,
}: HeroOrbitaProps) {
const { ref, active } = useMotionActive();
return (
<div
ref={ref}
className={`gcyo-root ${className}`}
style={style}
data-active={active}
>
<style>{CSS}</style>
<section className="gcyo-section">
<div className="gcyo-copy">
<h2>{title}</h2>
<p>{description}</p>
<a href={href}>
{label}
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="1.5"
aria-hidden="true"
>
<path d="M5 12h14m-6-6 6 6-6 6" />
</svg>
</a>
</div>
<div
className="gcyo-system"
aria-label="Sistema de conexiones ilustrativo"
>
<svg
className="gcyo-rings"
viewBox="0 0 400 400"
fill="none"
aria-hidden="true"
>
<circle cx="200" cy="200" r="155" />
<ellipse
cx="200"
cy="200"
rx="155"
ry="68"
transform="rotate(-35 200 200)"
/>
<ellipse
cx="200"
cy="200"
rx="155"
ry="68"
transform="rotate(35 200 200)"
/>
<circle className="gcyo-tracer" cx="200" cy="200" r="155" />
</svg>
<span className="gcyo-center">
Un punto.
<br />
Todo conectado.
</span>
{nodes.slice(0, 4).map((node, i) => (
<span
key={i}
className="gcyo-node"
style={{ "--gcyo-i": i } as CSSProperties}
>
{node}
</span>
))}
</div>
</section>
<div className="gcyo-bottom">
Un sistema abierto a lo que sigue.
<span>Concepto de producto · contenido de muestra</span>
</div>
</div>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/hero-orbitaEjemplo de uso de Hero de órbita interactiva
Después de copiar hero-orbita.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { HeroOrbita } from "./hero-orbita";
export default function Ejemplo() {
return <HeroOrbita title="Ejemplo" />;
}Propiedades de HeroOrbita
HeroOrbita acepta 7 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
description? | string | Opcional. |
label? | string | Opcional. |
href? | string | Opcional. |
nodes? | string[] | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Hero de órbita interactiva 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 |
|---|---|
--gcyo-fg | #202421 |
--gcyo-accent | #25724b |
--gcyo-bg | #10291f |
Accesibilidad y rendimiento
El archivo tiene 157 líneas (7 KB) y define 1 animación con @keyframes. Esto es lo que hace Hero de órbita interactiva 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".