Ir al contenido
Evaluar mi proyecto
Componentes · HerosDisponible

Hero 3D con puntero

Hero con una escena de tres capas que se inclina hacia el puntero.

Tu operación, en una sola pantalla.

Pedidos, existencias y entregas en el mismo lugar, con la información al día para todo el equipo.

Código completo

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

hero-3d-mouse.tsx134 líneas
Descargar
/**
 * Hero con una escena de tres capas (base, pantalla y tarjeta flotante) que se inclina
 * hacia el puntero con paralaje real: cada capa está a una profundidad distinta. La escena
 * es un tablero de pedidos con datos de ejemplo. Solo reacciona a ratón (no a toque) y
 * se queda quieta con movimiento reducido.
 *
 *   <Hero3dMouse
 *     title="Tu operación, en una sola pantalla."
 *     description="Pedidos, existencias y entregas en el mismo lugar."
 *     primary={{ label: "Ver cómo funciona", href: "#demo" }}
 *   />
 */

"use client";

import { useRef, type CSSProperties, type PointerEvent } from "react";

type Cta = { label: string; href: string };

export type Hero3dMouseProps = {
  title?: string;
  description?: string;
  primary?: Cta;
  secondary?: Cta;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gchm-root { position: relative; isolation: isolate; box-sizing: border-box; min-height: 720px; overflow: hidden; padding: clamp(48px, 6vw, 96px) clamp(20px, 5vw, 80px); display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); gap: clamp(32px, 5vw, 80px); align-items: center; background: #0a0a0b; color: #ededee; font: 16px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; }
.gchm-title { margin: 0; font-size: clamp(38px, 5.2vw, 76px); font-weight: 600; line-height: 1.02; letter-spacing: -.04em; text-wrap: balance; }
.gchm-desc { max-width: 46ch; margin: 22px 0 0; color: #9b9ca3; font-size: clamp(16px, 1.3vw, 18px); line-height: 1.6; }
.gchm-actions { display: flex; flex-wrap: wrap; gap: 12px; margin-top: 32px; }
.gchm-btn { display: inline-flex; align-items: center; min-height: 48px; padding: 0 22px; border-radius: 8px; font-size: 15px; font-weight: 600; text-decoration: none; transition: background-color .2s, border-color .2s; }
.gchm-btn[data-kind="primary"] { background: #ededee; color: #0a0a0b; }
.gchm-btn[data-kind="primary"]:hover { background: #fff; }
.gchm-btn[data-kind="ghost"] { border: 1px solid #34353b; color: #ededee; }
.gchm-btn[data-kind="ghost"]:hover { border-color: #5a5b63; background: #131315; }
.gchm-btn:focus-visible { outline: 2px solid #84b3ff; outline-offset: 3px; }
.gchm-stage { position: relative; perspective: 1500px; padding: 4% 0; }
.gchm-card { position: relative; aspect-ratio: 5 / 4; transform-style: preserve-3d; transition: transform .5s cubic-bezier(.16, 1, .3, 1); will-change: transform; }
.gchm-base { position: absolute; inset: 4% 3% 6% 3%; border: 1px solid #26272c; border-radius: 16px; background: #111113; box-shadow: 0 40px 50px -30px rgba(0, 0, 0, .9); }
.gchm-screen { position: absolute; inset: 9% 7% 12% 7%; display: flex; flex-direction: column; overflow: hidden; border: 1px solid #26272c; border-radius: 10px; background: #0a0a0b; transform: translateZ(26px); }
.gchm-head { display: flex; align-items: baseline; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid #1c1d21; font-size: 12.5px; color: #9b9ca3; }
.gchm-head b { color: #ededee; font-weight: 600; }
.gchm-row { display: grid; grid-template-columns: 4.5em 1fr auto; align-items: center; gap: 14px; padding: 11px 16px; border-bottom: 1px solid #1c1d21; font-size: 12.5px; }
.gchm-row:last-child { border-bottom: 0; }
.gchm-row code { color: #9b9ca3; font: 12px ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-variant-numeric: tabular-nums; }
.gchm-row span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gchm-row em { display: inline-flex; align-items: center; gap: 6px; color: #9b9ca3; font-style: normal; font-size: 12px; white-space: nowrap; }
.gchm-row em::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
.gchm-row em[data-tone="go"] { color: #84b3ff; }
.gchm-row em[data-tone="ok"] { color: #5fd38d; }
.gchm-row em[data-tone="late"] { color: #ff8f84; }
.gchm-float { position: absolute; right: -2%; bottom: 4%; width: 42%; padding: 14px 16px; border: 1px solid #34353b; border-radius: 12px; background: #1a1b1e; transform: translateZ(80px); box-shadow: 0 24px 36px -18px rgba(0, 0, 0, .9); }
.gchm-float strong { display: block; font-size: 14px; font-weight: 600; }
.gchm-float small { display: block; margin-top: 2px; color: #9b9ca3; font-size: 12px; }
.gchm-steps { display: grid; grid-template-columns: repeat(3, 1fr); gap: 4px; margin-top: 12px; }
.gchm-steps i { height: 3px; border-radius: 2px; background: #34353b; }
.gchm-steps i[data-on="true"] { background: #84b3ff; }
.gchm-note { position: absolute; left: 4%; bottom: -2%; margin: 0; color: #70717a; font-size: 12px; }
@media (max-width: 900px) { .gchm-root { grid-template-columns: 1fr; } .gchm-float { right: 0; } }
@media (prefers-reduced-motion: reduce) { .gchm-card { transition: none; } }
`;

const ORDERS = [
  { id: "#4821", who: "Entrega en Monterrey", state: "En ruta", tone: "go" },
  { id: "#4822", who: "Recolección en bodega", state: "Listo", tone: "ok" },
  { id: "#4833", who: "Entrega en Querétaro", state: "Retraso", tone: "late" },
  { id: "#4834", who: "Entrega en Puebla", state: "En ruta", tone: "go" },
  { id: "#4840", who: "Recolección en bodega", state: "Preparando", tone: "" },
];

export function Hero3dMouse({
  title = "Tu operación, en una sola pantalla.",
  description = "Pedidos, existencias y entregas en el mismo lugar, con la información al día para todo el equipo.",
  primary = { label: "Ver cómo funciona", href: "#demo" },
  secondary = { label: "Hablar con el equipo", href: "#contacto" },
  className,
  style,
}: Hero3dMouseProps) {
  const cardRef = useRef<HTMLDivElement>(null);

  const onMove = (e: PointerEvent<HTMLDivElement>) => {
    const el = cardRef.current;
    if (!el || e.pointerType !== "mouse" || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
    const rect = e.currentTarget.getBoundingClientRect();
    const x = (e.clientX - rect.left) / rect.width - 0.5;
    const y = (e.clientY - rect.top) / rect.height - 0.5;
    el.style.transform = `rotateX(${(-y * 10).toFixed(2)}deg) rotateY(${(x * 12).toFixed(2)}deg)`;
  };

  const onLeave = () => {
    if (cardRef.current) cardRef.current.style.transform = "";
  };

  return (
    <>
      <style>{CSS}</style>
      <section className={["gchm-root", className].filter(Boolean).join(" ")} style={style}>
        <div>
          <h1 className="gchm-title">{title}</h1>
          <p className="gchm-desc">{description}</p>
          <div className="gchm-actions">
            <a className="gchm-btn" data-kind="primary" href={primary.href}>{primary.label}</a>
            <a className="gchm-btn" data-kind="ghost" href={secondary.href}>{secondary.label}</a>
          </div>
        </div>
        <div className="gchm-stage" onPointerMove={onMove} onPointerLeave={onLeave} aria-hidden="true">
          <div className="gchm-card" ref={cardRef}>
            <div className="gchm-base" />
            <div className="gchm-screen">
              <div className="gchm-head"><b>Pedidos de hoy</b><span>5 de 12</span></div>
              {ORDERS.map((o) => (
                <div key={o.id} className="gchm-row">
                  <code>{o.id}</code>
                  <span>{o.who}</span>
                  <em data-tone={o.tone}>{o.state}</em>
                </div>
              ))}
            </div>
            <div className="gchm-float">
              <strong>Pedido #4821</strong>
              <small>Salió de bodega a las 09:40</small>
              <div className="gchm-steps"><i data-on="true" /><i data-on="true" /><i /></div>
            </div>
            <p className="gchm-note">Datos de ejemplo</p>
          </div>
        </div>
      </section>
    </>
  );
}

Ú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-3d-mouse

Ejemplo de uso de Hero 3D con puntero

Después de copiar hero-3d-mouse.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { Hero3dMouse } from "./hero-3d-mouse";

export default function Ejemplo() {
  return <Hero3dMouse title="Ejemplo" />;
}

Propiedades de Hero3dMouse

Hero3dMouse acepta 6 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
title?stringOpcional.
description?stringOpcional.
primary?CtaOpcional.
secondary?CtaOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Accesibilidad y rendimiento

El archivo tiene 134 líneas (8 KB) y no necesita @keyframes. Esto es lo que hace Hero 3D con puntero 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-*.
  • Reacciona al puntero con eventos pointer (ratón, lápiz y táctil).
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18

Acerca del producto

Una base, una pantalla y una tarjeta flotante a profundidades distintas dan paralaje real. La escena es un tablero de pedidos con datos de ejemplo. Solo reacciona a ratón y se detiene con movimiento reducido.

Incluye

Un solo archivo React + TypeScript con estilos dentro. Sin Tailwind ni dependencias externas.

  • Tres capas con profundidad real
  • Inclinación solo con ratón
  • Escena oculta a lectores de pantalla
  • Se apila en una columna en móvil
  • Respeta prefers-reduced-motion