Ir al contenido
Evaluar mi proyecto
Componentes · HerosDisponible

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 sistema
Un punto.
Todo conectado.
IdeasEquipoDiseñoProducto
Un sistema abierto a lo que sigue.Concepto de producto · contenido de muestra

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

hero-orbita.tsx157 líneas
Descargar
"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.

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

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

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

PropiedadTipoDescripción
title?stringOpcional.
description?stringOpcional.
label?stringOpcional.
href?stringOpcional.
nodes?string[]Opcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

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.

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