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.
Código completo
Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.
"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.
npx shadcn@latest add https://www.gassierconsulting.com/r/scroll-zoomEjemplo 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.
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.
| Propiedad | Tipo | Descripción |
|---|---|---|
title? | string | Opcional. |
caption? | string | Opcional. |
height? | number | Opcional. |
className? | string | Opcional. |
style? | CSSProperties | Opcional. |
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.
| Variable | Valor por defecto |
|---|---|
--h | 460px |
--p | 0 |
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".