Scroll-based Image Resizing

Creating a Scroll-Responsive Image Component with Dynamic Resizing and Opacity Transitions in React and TypeScript

This guide explains how to build a highly interactive and scroll-responsive image container using React, TypeScript, and CSS Modules. The effect dynamically resizes an image and modifies its border-radius as the user scrolls down the page. Additionally, it features a smooth "fade and lift" text animation and a controlled "freeze" zone before the animation starts, ensuring a clean and readable entry.

Technologies Used

Inspired by the stars, moved by the night.

Lorem ipsum dolor sit amet, consectetur adipiscing elit.

How to Build a Scroll-Responsive Image Component in React

To recreate this effect from scratch, we use a combination of React state management, scroll event handling, the IntersectionObserver API, and conditional rendering logic. Each image container is treated as an independent and self-contained unit that resizes and transitions based on the user's scroll behavior. Let's break down every part of the process to help you fully understand and replicate it.


1.   Component Structure and Props: The component is a functional React component that receives three props: an image URL (src), a title (title), and a paragraph of text (paragraph). These props define the visual content of each image container.

1interface ScrollImageContainerProps {
2  src: string;
3  title: string;
4  paragraph: string;
5}

2.   Initial State and Refs: Internally, we use React's useRef hook to reference the image container and the text overlay, and useState to manage various dynamic properties.

These include the width and height of the container, the border radius to create the rounded corners effect, the opacity and vertical translation of the text for the fade-out effect on scroll, and a frozen state to delay the resize animation until a scroll threshold is passed. All of these states are initialized with default values appropriate for the component's full-screen layout when it first enters the viewport.

3.   Initial Size Setup: When the component mounts, it immediately calculates the initial size of the image container. On larger screens, the image can cover almost the entire viewport, while on smaller screens like mobile, it's slightly reduced to ensure usability. This size is set using the current dimensions of the browser window, ensuring the image appears large and fully visible when first rendered.

To access the browser's dimensions, we use window.innerWidth and window.innerHeight, which return the current width and height of the viewport in pixels. These values help us determine whether the screen is considered small (like on a mobile device) or large (like on a desktop).

Based on this, we apply different base values for width and height. For example, if window.innerWidth is below a certain threshold (e.g., 768 pixels), we reduce the image's initial size slightly to keep the layout user-friendly on smaller screens.

1let newWidth = window.innerWidth - effectiveScroll * 0.4;
2      let newHeight = window.innerHeight - effectiveScroll * 0.4;
3
4      newWidth = window.innerWidth < 768
5        ? Math.max(newWidth, window.innerWidth * 0.75)
6        : Math.max(newWidth, window.innerWidth * 0.8);
7
8      newHeight = window.innerWidth < 768
9        ? Math.max(newHeight, window.innerHeight * 0.85)
10        : Math.max(newHeight, window.innerHeight * 0.9);

4.   IntersectionObserver: Starting the Scroll Behavior: The component uses the IntersectionObserver API to detect when the image container enters the user's viewport. Only after this happens does the scroll-based behavior begin. When the observer detects that the image is on screen, it removes the initial "frozen" state and attaches a scroll event listener to the window. This approach ensures that the scroll event only triggers logic when the image is visible, improving performance and user experience.

Drawn by his own hand, seen through a storm within.

Vivamus luctus urna sed urna ultricies ac tempor dui sagittis.

5.   "Freeze" Behavior Before Resizing: To prevent the image from resizing immediately, we introduce a "freeze distance" — a pixel threshold (for example, 400px) that must be scrolled past before resizing begins. Until this threshold is crossed, the image maintains its full size. This is done by calculating the image container's position on the page and comparing it to the current scroll position. If the user hasn't scrolled enough, no resizing occurs, preserving the initial visibility.

6.   Handling Scroll-Based Resizing: Once the freeze is lifted, we start recalculating the image's size on each scroll. The new width and height are calculated by subtracting a percentage of the scroll distance from the original viewport size.

To avoid excessive shrinking, minimum width and height limits are applied. These limits are adaptive depending on whether the screen is small or large. Simultaneously, the border-radius increases gradually based on the scroll distance, up to a predefined limit (like 30px).

This smooth increase gives the image a rounded look as it shrinks. These values are updated using React's state to trigger smooth CSS transitions with transition properties applied via class names or inline styles.

7.   Text Opacity and Movement: The text overlay, which contains the title and paragraph, fades out and moves upward as the user scrolls. This is handled by calculating the text block's position within the viewport. Based on how much of the element is visible, we compute a visibility ratio.

The opacity is then derived from this ratio using a non-linear function — for example, raising it to a power like 6 — to create a more natural fade. In parallel, the text is translated upward using a translateY value that grows as the opacity decreases, giving the effect of the text floating away.

8.   Independent State Per Instance: Each instance of the image component maintains its own state and observer. This design is crucial to allow multiple components on the same page to function independently. This means one image can begin shrinking while another remains full-size, only the image currently in view is responding to scroll, and each instance is frozen and unfrozen separately based on its own position and scroll interaction.

9.   Cleanup and Performance Considerations: When the component unmounts, the scroll event listener is removed and the observer is disconnected. This is critical to avoid memory leaks and unnecessary performance overhead. Additionally, because the observer only triggers once the image is visible and the scroll listener only activates after that, the system is highly optimized.

10.   Styling and Transitions: The visual transitions are implemented using CSS transitions for properties such as width, height, border-radius, opacity, and transform. To style the image container, a background image is applied via CSS, with background-size set to cover and background-position centered. A responsive class system is used to apply different values for padding, margins, and sizing depending on the screen size.

Final Thoughts

This component combines multiple techniques — useRef, useState, useEffect, IntersectionObserver, scroll events, and responsive styling — to create an interactive and immersive visual experience. By treating each image container as a self-contained, scroll-responsive unit, we achieve a high level of interactivity without compromising performance.
This pattern can be reused and extended for other scroll-based animations, making it a great starting point for more complex interactions in React.