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 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.
npx shadcn@latest add https://www.gassierconsulting.com/r/hero-3d-mouseEjemplo 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.
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.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
description? | string | Opcional. |
primary? | Cta | Opcional. |
secondary? | Cta | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
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".
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