Ir al contenido
Evaluar mi proyecto
Componentes · ScrollDisponible

Imagen que crece hasta ocupar todo

Una tarjeta pequeña se expande a pantalla completa mientras el titular se desvanece y aparece el texto.

Una idea que se hace grande

Del boceto en una servilleta a un sistema que sostiene tu operación.

Desplázate ↓

Código completo

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

scroll-zoom.tsx68 líneas
Descargar
"use client";

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

/** Imagen que crece con el scroll: una tarjeta pequeña con esquinas redondeadas se expande hasta ocupar todo el escenario mientras el titular se desvanece y aparece el texto. Autocontenida en su panel. React + TypeScript, sin dependencias. */
export type ScrollZoomProps = {
  title?: string;
  caption?: string;
  height?: number;
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcszo-root { box-sizing: border-box; position: relative; width: 100%; height: var(--h, 460px); overflow-y: auto; overflow-x: hidden; background: #f4f1ea; color: #17150f; font: 400 16px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; scrollbar-width: thin; outline: 0; }
.gcszo-root *, .gcszo-root *::before, .gcszo-root *::after { box-sizing: border-box; }
.gcszo-root:focus-visible { box-shadow: inset 0 0 0 2px #17150f; }
.gcszo-track { height: calc(var(--h, 460px) * 2.2); }
.gcszo-stage { position: sticky; top: 0; height: var(--h, 460px); display: grid; place-items: center; overflow: hidden; }
.gcszo-title { position: absolute; z-index: 2; top: 9%; margin: 0; padding: 0 20px; text-align: center; font: 500 clamp(32px, 8vw, 96px)/.95 "Newsreader", Georgia, ui-serif, serif; letter-spacing: -.04em; color: #17150f; opacity: calc(1 - var(--p, 0) * 2.2); transform: translateY(calc(var(--p, 0) * -40px)) scale(calc(1 + var(--p, 0) * .2)); mix-blend-mode: normal; }
.gcszo-frame { position: relative; margin-top: calc((1 - var(--p, 0)) * 18%); width: calc(34% + var(--p, 0) * 66%); height: calc(38% + var(--p, 0) * 62%); border-radius: calc(28px * (1 - var(--p, 0))); overflow: hidden; background: linear-gradient(135deg, #ff7a59, #ffb45e 40%, #6b4bd8); will-change: width, height; box-shadow: 0 30px 60px -24px rgba(0, 0, 0, .4); }
.gcszo-frame svg { position: absolute; inset: 0; width: 100%; height: 100%; transform: scale(calc(1.15 - var(--p, 0) * .15)); }
.gcszo-cap { position: absolute; z-index: 3; left: 0; right: 0; bottom: 8%; display: grid; justify-items: center; padding: 0 24px; text-align: center; color: #fff; opacity: calc((var(--p, 0) - .55) * 3); transform: translateY(calc((1 - var(--p, 0)) * 20px)); }
.gcszo-cap p { margin: 0; max-width: 28ch; font: 500 clamp(22px, 4vw, 40px)/1.1 "Newsreader", Georgia, ui-serif, serif; letter-spacing: -.02em; text-shadow: 0 2px 20px rgba(0, 0, 0, .4); }
.gcszo-hint { position: absolute; z-index: 3; left: 50%; bottom: 12px; transform: translateX(-50%); font-size: 12px; color: #6e685b; opacity: calc(1 - var(--p, 0) * 6); }
@media (prefers-reduced-motion: reduce) { .gcszo-frame { width: 100% !important; height: 100% !important; border-radius: 0 !important; } .gcszo-title { opacity: 0 !important; } .gcszo-cap { opacity: 1 !important; transform: none !important; } }`;

export function ScrollZoom({ title = "Una idea que se hace grande", caption = "Del boceto en una servilleta a un sistema que sostiene tu operación.", height = 460, className = "", style }: ScrollZoomProps) {
  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(Math.min(1, (max > 0 ? el.scrollTop / max : 0) * 1.25)));
    };
    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={`gcszo-root ${className}`} style={{ "--h": `${height}px`, ...style } as CSSProperties} tabIndex={0} role="region" aria-label="Imagen que crece al desplazar">
      <style>{CSS}</style>
      <div className="gcszo-track">
        <div className="gcszo-stage">
          <h2 className="gcszo-title">{title}</h2>
          <div className="gcszo-frame" aria-hidden="true">
            <svg viewBox="0 0 400 300" preserveAspectRatio="xMidYMid slice">
              <circle cx="290" cy="110" r="52" fill="#fff4cf" opacity=".95" />
              <path d="M0 230 L90 140 L170 215 L260 120 L400 240 V300 H0Z" fill="#2b1a5e" opacity=".75" />
              <path d="M0 270 L120 200 L230 260 L330 195 L400 250 V300 H0Z" fill="#170d3a" />
            </svg>
          </div>
          <div className="gcszo-cap">
            <p>{caption}</p>
          </div>
          <span className="gcszo-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-zoom

Ejemplo de uso de Imagen que crece hasta ocupar todo

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

ejemplo.tsx5 líneas
import { ScrollZoom } from "./scroll-zoom";

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

Propiedades de ScrollZoom

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

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

Personaliza Imagen que crece hasta ocupar todo 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 68 líneas (5 KB) y no necesita @keyframes. Esto es lo que hace Imagen que crece hasta ocupar todo 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)