Ir al contenido
Evaluar mi proyecto
Componentes · ScrollDisponible

Escena con paralaje de montañas

Sol, tres capas de montañas y un titular se mueven a velocidades distintas mientras la niebla se despeja.

Sube la montaña

Cada capa se mueve a su propio ritmo y crea profundidad sin una sola imagen pesada.

Desplázate ↓

Código completo

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

scroll-paralaje.tsx74 líneas
Descargar
"use client";

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

/** Escena con paralaje: sol, tres capas de montañas y un titular se mueven a velocidades distintas mientras el contenido sube, con niebla que se despeja. Autocontenida en su panel. Capas SVG propias. React + TypeScript, sin dependencias. */
export type ScrollParalajeProps = {
  title?: string;
  text?: string;
  height?: number;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcspl-root { box-sizing: border-box; position: relative; width: 100%; height: var(--h, 460px); overflow-y: auto; overflow-x: hidden; background: #0f1a33; color: #fff; font: 400 16px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; scrollbar-width: thin; outline: 0; }
.gcspl-root *, .gcspl-root *::before, .gcspl-root *::after { box-sizing: border-box; }
.gcspl-root:focus-visible { box-shadow: inset 0 0 0 2px #fff; }
.gcspl-track { position: relative; height: calc(var(--h, 460px) * 2.1); }
.gcspl-stage { position: sticky; top: 0; height: var(--h, 460px); overflow: hidden; background: linear-gradient(180deg, #1b2a5e 0%, #7a4b9c 45%, #ff9a6b 78%, #ffd6a0 100%); }
.gcspl-l { position: absolute; left: -4%; right: -4%; bottom: 0; will-change: transform; transform: translateY(calc(var(--p, 0) * var(--f) * 1px)); }
.gcspl-l svg { display: block; width: 100%; height: auto; }
.gcspl-sun { position: absolute; left: 50%; bottom: 40%; width: 22%; aspect-ratio: 1; margin-left: -11%; border-radius: 50%; background: #fff2c2; box-shadow: 0 0 80px 30px rgba(255, 220, 160, .55); transform: translateY(calc(var(--p, 0) * 90px)); }
.gcspl-copy { position: absolute; left: 0; right: 0; top: 14%; display: grid; justify-items: center; gap: 10px; padding: 0 20px; text-align: center; transform: translateY(calc(var(--p, 0) * -180px)); opacity: calc(1 - var(--p, 0) * 1.4); }
.gcspl-copy h2 { margin: 0; font: 700 clamp(30px, 7vw, 68px)/.98 ui-sans-serif, system-ui, sans-serif; letter-spacing: -.045em; text-shadow: 0 4px 30px rgba(20, 10, 60, .4); }
.gcspl-copy p { margin: 0; max-width: 36ch; opacity: .92; }
.gcspl-fog { position: absolute; inset: 0; background: linear-gradient(180deg, transparent 50%, rgba(255, 233, 210, .85)); opacity: calc(1 - var(--p, 0) * 1.2); pointer-events: none; }
.gcspl-hint { position: absolute; left: 50%; bottom: 14px; transform: translateX(-50%); font-size: 12px; opacity: .8; }
@media (prefers-reduced-motion: reduce) { .gcspl-l, .gcspl-sun, .gcspl-copy { transform: none !important; } }`;

export function ScrollParalaje({ title = "Sube la montaña", text = "Cada capa se mueve a su propio ritmo y crea profundidad sin una sola imagen pesada.", height = 460, className = "", style }: ScrollParalajeProps) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    let raf = 0;
    const update = () => {
      raf = 0;
      const max = el.scrollHeight - el.clientHeight;
      el.style.setProperty("--p", String(max > 0 ? el.scrollTop / max : 0));
    };
    const onScroll = () => { if (!raf) raf = requestAnimationFrame(update); };
    update();
    el.addEventListener("scroll", onScroll, { passive: true });
    return () => { el.removeEventListener("scroll", onScroll); cancelAnimationFrame(raf); };
  }, []);

  return (
    <div ref={ref} className={`gcspl-root ${className}`} style={{ "--h": `${height}px`, ...style } as CSSProperties} tabIndex={0} role="region" aria-label="Escena con paralaje: desplázate">
      <style>{CSS}</style>
      <div className="gcspl-track">
        <div className="gcspl-stage">
          <div className="gcspl-sun" aria-hidden="true" />
          <div className="gcspl-l" style={{ "--f": 40 } as CSSProperties} aria-hidden="true">
            <svg viewBox="0 0 1200 300" preserveAspectRatio="none"><path d="M0 300V190l120-60 130 70 140-90 150 80 130-60 160 90 150-70 120 50v190z" fill="#5a3b84" opacity=".7" /></svg>
          </div>
          <div className="gcspl-l" style={{ "--f": 90 } as CSSProperties} aria-hidden="true">
            <svg viewBox="0 0 1200 300" preserveAspectRatio="none"><path d="M0 300V210l100-50 150 80 120-100 160 120 120-70 150 90 140-80 160 70v40z" fill="#3a2a66" opacity=".88" /></svg>
          </div>
          <div className="gcspl-l" style={{ "--f": 160 } as CSSProperties} aria-hidden="true">
            <svg viewBox="0 0 1200 300" preserveAspectRatio="none"><path d="M0 300V230l140-70 130 90 160-110 150 100 130-60 170 90 150-40 170 60v40z" fill="#1d1640" /></svg>
          </div>
          <div className="gcspl-fog" aria-hidden="true" />
          <div className="gcspl-copy">
            <h2>{title}</h2>
            <p>{text}</p>
          </div>
          <span className="gcspl-hint">Desplázate ↓</span>
        </div>
      </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/scroll-paralaje

Ejemplo de uso de Escena con paralaje de montañas

Después de copiar scroll-paralaje.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { ScrollParalaje } from "./scroll-paralaje";

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

Propiedades de ScrollParalaje

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

PropiedadTipoDescripción
title?stringOpcional.
text?stringOpcional.
height?numberOpcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Escena con paralaje de montañas 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
--h460px
--p0

Accesibilidad y rendimiento

El archivo tiene 74 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Escena con paralaje de montañas 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).
  • Declara roles ARIA para lectores de pantalla: region.
  • Anima con requestAnimationFrame, sincronizado con el refresco de pantalla.
  • 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)