Hero con pila isométrica 3D
Cuatro paneles de interfaz flotan en profundidad, se separan al apuntarlos y se inclinan con el puntero. Solo CSS 3D.
Cada capa de tu negocio, a la vista
Datos, procesos, integraciones y reportes apilados en un solo sistema. Apunta la pila para separarlas.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"use client";
import { useRef, type CSSProperties, type PointerEvent } from "react";
/** Hero con pila isométrica de paneles: cuatro capas de interfaz flotan en profundidad, se separan al apuntarlas y se inclinan con el puntero. Solo CSS 3D. React + TypeScript, sin dependencias. */
export type HeroPila3dProps = {
title?: string;
description?: string;
primary?: string;
secondary?: string;
href?: string;
layers?: string[];
className?: string;
style?: CSSProperties;
};
const CSS = `
.gchpl-root { box-sizing: border-box; position: relative; container-type: inline-size; width: 100%; min-height: 640px; overflow: hidden; color: #e9ecf6; background: radial-gradient(90% 80% at 80% 40%, #1a2150, #090b1b 70%); font: 500 16px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gchpl-root *, .gchpl-root *::before, .gchpl-root *::after { box-sizing: border-box; }
.gchpl-root a { -webkit-tap-highlight-color: transparent; }
.gchpl-root :focus-visible { outline: 2px solid #8fa8ff; outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { .gchpl-root *, .gchpl-root *::before, .gchpl-root *::after { transition-duration: .01ms !important; animation: none !important; } }
.gchpl-grid { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.05fr); align-items: center; gap: 24px; min-height: 640px; padding: 56px clamp(24px, 6cqi, 80px); }
.gchpl-title { margin: 0 0 20px; max-width: 13ch; font: 700 clamp(40px, 6.4cqi, 80px)/1 ui-sans-serif, system-ui, -apple-system, sans-serif; letter-spacing: -.04em; text-wrap: balance; }
.gchpl-desc { margin: 0 0 30px; max-width: 42ch; color: #a9b2d6; font-size: clamp(15px, 1.7cqi, 19px); text-wrap: pretty; }
.gchpl-actions { display: flex; flex-wrap: wrap; gap: 12px; }
.gchpl-btn { display: inline-flex; align-items: center; min-height: 50px; padding: 0 26px; border-radius: 10px; font-weight: 600; text-decoration: none; color: #e9ecf6; border: 1px solid #3a4478; transition: background .2s, transform .25s cubic-bezier(.16, 1, .3, 1); }
.gchpl-btn:hover { background: rgba(120, 140, 255, .14); transform: translateY(-2px); }
.gchpl-btn[data-p="true"] { background: #6f86ff; border-color: #6f86ff; color: #070a22; }
.gchpl-btn[data-p="true"]:hover { background: #8da0ff; }
.gchpl-scene { position: relative; height: 480px; perspective: 1400px; }
.gchpl-stack { --rx: 58deg; --rz: -38deg; position: absolute; left: 50%; top: 50%; width: 330px; height: 330px; margin: -165px 0 0 -165px; transform-style: preserve-3d; transform: rotateX(var(--rx)) rotateZ(var(--rz)); transition: transform .6s cubic-bezier(.16, 1, .3, 1); }
.gchpl-layer { position: absolute; inset: 0; border-radius: 22px; transform: translateZ(calc(var(--i) * var(--gap, 46px))); transition: transform .7s cubic-bezier(.16, 1, .3, 1); background: linear-gradient(145deg, color-mix(in srgb, var(--c) 28%, #0d1230), color-mix(in srgb, var(--c) 10%, #0a0d24)); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--c) 60%, transparent), inset 0 1px 0 rgba(255, 255, 255, .22), 0 40px 40px -20px rgba(0, 0, 0, .6); padding: 22px; }
.gchpl-scene:hover .gchpl-stack, .gchpl-scene:focus-within .gchpl-stack { --gap: 74px; }
.gchpl-scene:hover .gchpl-layer, .gchpl-scene:focus-within .gchpl-layer { transform: translateZ(calc(var(--i) * 74px)); }
.gchpl-tag { display: inline-block; padding: 4px 10px; border-radius: 6px; background: color-mix(in srgb, var(--c) 40%, transparent); font-size: 12px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase; color: #fff; }
.gchpl-bars { position: absolute; left: 22px; right: 22px; bottom: 22px; display: flex; align-items: flex-end; gap: 8px; height: 90px; }
.gchpl-bars i { flex: 1; border-radius: 4px 4px 0 0; background: color-mix(in srgb, var(--c) 70%, #fff); opacity: .85; height: var(--h); }
.gchpl-rows { position: absolute; left: 22px; right: 22px; top: 62px; display: grid; gap: 10px; }
.gchpl-rows i { display: block; height: 10px; border-radius: 6px; background: color-mix(in srgb, var(--c) 36%, transparent); }
.gchpl-rows i:nth-child(2) { width: 72%; } .gchpl-rows i:nth-child(3) { width: 48%; }
@container (max-width: 800px) { .gchpl-grid { grid-template-columns: minmax(0, 1fr); } .gchpl-scene { height: 360px; } .gchpl-stack { width: 240px; height: 240px; margin: -120px 0 0 -120px; } }`;
const COLORS = ["#6f86ff", "#5de0c0", "#ffb86b", "#ff7ab8"];
export function HeroPila3d({ title = "Cada capa de tu negocio, a la vista", description = "Datos, procesos, integraciones y reportes apilados en un solo sistema. Apunta la pila para separarlas.", primary = "Ver cómo funciona", secondary = "Hablar con el equipo", href = "#", layers = ["Datos", "Procesos", "Integraciones", "Reportes"], className = "", style }: HeroPila3dProps) {
const scene = useRef<HTMLDivElement>(null);
function move(event: PointerEvent<HTMLDivElement>) {
if (event.pointerType === "touch") return;
const rect = event.currentTarget.getBoundingClientRect();
const x = (event.clientX - rect.left) / rect.width - 0.5;
const y = (event.clientY - rect.top) / rect.height - 0.5;
const stack = scene.current?.firstElementChild as HTMLElement | null;
stack?.style.setProperty("--rx", `${58 - y * 14}deg`);
stack?.style.setProperty("--rz", `${-38 + x * 22}deg`);
}
function leave() {
const stack = scene.current?.firstElementChild as HTMLElement | null;
stack?.style.setProperty("--rx", "58deg");
stack?.style.setProperty("--rz", "-38deg");
}
return (
<section className={`gchpl-root ${className}`} style={style}>
<style>{CSS}</style>
<div className="gchpl-grid">
<div>
<h1 className="gchpl-title">{title}</h1>
<p className="gchpl-desc">{description}</p>
<div className="gchpl-actions">
<a className="gchpl-btn" data-p="true" href={href}>
{primary}
</a>
<a className="gchpl-btn" href={href}>
{secondary}
</a>
</div>
</div>
<div ref={scene} className="gchpl-scene" onPointerMove={move} onPointerLeave={leave} aria-hidden="true">
<div className="gchpl-stack">
{layers.map((name, index) => (
<div key={name} className="gchpl-layer" style={{ "--i": index, "--c": COLORS[index % COLORS.length] } as CSSProperties}>
<span className="gchpl-tag">{name}</span>
{index % 2 === 0 ? (
<span className="gchpl-bars">
{[40, 62, 48, 80, 58, 90].map((height, i) => (
<i key={i} style={{ "--h": `${height}%` } as CSSProperties} />
))}
</span>
) : (
<span className="gchpl-rows">
<i />
<i />
<i />
</span>
)}
</div>
))}
</div>
</div>
</div>
</section>
);
}
Úsalo en tu proyecto
Instálalo con la CLI de shadcn.
npx shadcn@latest add https://www.gassierconsulting.com/r/hero-pila-3dEjemplo de uso de Hero con pila isométrica 3D
Después de copiar hero-pila-3d.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.
import { HeroPila3d } from "./hero-pila-3d";
export default function Ejemplo() {
return <HeroPila3d title="Ejemplo" />;
}Propiedades de HeroPila3d
HeroPila3d acepta 8 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? | string | Opcional. |
secondary? | string | Opcional. |
href? | string | Opcional. |
layers? | string[] | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
Personaliza Hero con pila isométrica 3D 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 |
|---|---|
--gap | 46px |
Accesibilidad y rendimiento
El archivo tiene 106 líneas (7 KB) y no necesita @keyframes. Esto es lo que hace Hero con pila isométrica 3D 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).
- Calcula tonos con color-mix(), así que un solo color base alcanza.
- Es un componente de cliente de Next.js: incluye la directiva "use client".