Make text come to life as the user scrolls across a fixed positioned div

As the user scrolls, I am attempting to animate four headings. First, I create a sticky positioned div. Then, as the user scrolls over the headings, each one toggles the class .active sequentially, with the last one remaining visible and the scrolling continuing to the final div.

The issue I'm encountering is that once the .sticky div becomes sticky, the browser stops detecting further scrolling until the div returns to a non-sticky state.

--- UPDATE

I've made adjustments to the code to transition sequentially, which has improved the functionality. However, I would like the animations to start only when the .sticky div becomes sticky. When I attempted this, the scroll animation completely stopped working. Additionally, I want to keep all the headings on the same block. If I apply position: absolute to them, it disrupts the animations as well.

const headings = Array.from(document.querySelectorAll('.animated-text'));
const sticky = document.querySelector('.sticky');

let currentActive = null;

window.addEventListener('scroll', () => {
  const viewportHeight = window.innerHeight;
  headings.forEach((heading, index) => {
    const headingRect = heading.getBoundingClientRect();

    if (headingRect.top <= viewportHeight / 2) {
      if (currentActive) {
        currentActive.classList.remove('active');
      }
      heading.classList.add('active');
      currentActive = heading;
    }
  });
});
body {
  margin: 0
}

section {
  position: relative;
}

.sticky {
  padding-bottom: 150px;
  background: #2d232c;
  position: sticky;
  top: 0;
  overflow: hidden;
  height: auto;
  color: white;
}

.animated-text {
  opacity: 0;
  height: 0;
  overflow: hidden;
  transition: opacity 1s ease, height 1s ease, transform 1s ease;
  transform: translateY(0);
}

.animated-text.active {
  height: auto;
  opacity: 1;
  transform: translateY(-20px);
}

.hero, .end {
  height: 100px;
  background: white;
}
<section class='hero'>
  <p>Start</p>
</section>

<section class='sticky'>
  <div class='text-animations'>
    <h1 class='animated-text active'>Intro</h1>
    <h2 class='animated-text'>First</h2>
    <h2 class='animated-text'>Second</h2>
    <h2 class='animated-text'>Third</h2>
  </div>
  <p class='intro_content'>
     Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam Lorem ipsum dolor sit... 
  </p>  
</section>

<section class='end'>
  <p>End<p>
</section>

Answer №1

When attempting to create a captivating animation effect for your headings as the user scrolls and the sticky div comes into view, be aware that the current code only manages the scroll event once. Additionally, the scrolling halts after the .sticky div becomes sticky.

To accomplish the desired effect of toggling the "active" class for the headings sequentially as the user scrolls, you must consistently monitor the scroll position and update the active heading accordingly. This can be achieved by utilizing the getBoundingClientRect() method to ascertain the positions of the headings relative to the viewport.

const headings = document.querySelectorAll('.animated-text');
const sticky = document.querySelector('.sticky');

window.addEventListener('scroll', () => {
  const stickyRect = sticky.getBoundingClientRect();

  headings.forEach((heading, index) => {
    const headingRect = heading.getBoundingClientRect();

    // Determine if the heading is within the sticky div and visible
    if (headingRect.top >= stickyRect.top && headingRect.bottom <= stickyRect.bottom) {
      // Add 'active' class to the current heading and remove from others
      heading.classList.add('active');
      for (let i = 0; i < headings.length; i++) {
        if (i !== index) {
          headings[i].classList.remove('active');
        }
      }
    }
  });
});
body {
  margin: 0;
}
section {
  position: relative;
}
.sticky {
  padding-bottom: 150px;
  background: #2d232c;
  position: sticky;
  top: 0;
  overflow: hidden;
  height: auto;
  color: white;
}
.animated-text {
  opacity: 0;
  max-height: 0;
  transition: opacity 1s ease, max-height 1s ease;
}
.animated-text.active {
  max-height: 1000px;
  opacity: 1;   
}
.hero, .end {
  height: 100px;
  background: white;
}
<!DOCTYPE html>
<html>
<head>
  <title>Scroll Animation</title>
 
</head>
<body>
  <section class='hero'>
    <p>Hero space</p>
  </section>

  <section class='sticky'>
    <div class='text-animations'>
      <h1 class='animated-text active'>Intro</h1>
      <h2 class='h1 animated-text'>First</h2>
      <h2 class='h1 animated-text'>Second</h2>
      <h2 class='h1 animated-text'>Third</h2>
      <h3 class='intro_content'>
        Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam Lorem ipsum dolor sit amet, consectetur adipisicing ... 

With this refined code, the browser will continuously detect further scrolling and adapt the "active" class for the headings as the user scrolls and the .sticky div turns sticky. This enhanced functionality delivers the intended animation effect for the headings in a seamless, engaging manner.

Answer №2

To conform to the requirement of positioning all headings at the top, simply include

position:absolute; 
top:0;

within the .active class, which effectively addresses the issue.

Additionally, in order for the animation to initiate only when the div becomes sticky, it is imperative to compare the position of the stickyDiv and then implement the animation logic accordingly.

I have also attempted to incorporate a logic for smoothly transitioning the headings based on scroll direction, ensuring that the heading transitions occur correctly irrespective of the scroll movement.

let headings = Array.from(document.querySelectorAll('.animated-text'));
const sticky = document.querySelector('.sticky');
let lastScrollTop = 0;
let currentActive=0;
window.addEventListener('scroll', () => {
  const stickyDiv=sticky.getBoundingClientRect(); 
  let st = window.pageYOffset || document.documentElement.scrollTop; 
  if (st > lastScrollTop) {
     if (stickyDiv.top===0) {
     for(let i=currentActive;i<headings.length-1;i++)
        {
         const headingRect = headings[i].getBoundingClientRect();
         if(headingRect.top<=0){
                currentActive=i+1;
                headings[i].classList.remove('active');
                headings[i+1].classList.add('active');
         }
     }
  }
} else if (st < lastScrollTop) {
    if (stickyDiv.top===0) {
    for(let i=currentActive;i>0;i--)
    {
     const headingRect = headings[i].getBoundingClientRect();
     if(headingRect.top<=0){
            currentActive=i-1;
            headings[i].classList.remove('active');
            headings[i-1].classList.add('active');
     }
   }
  } }
  if(stickyDiv.top>0){
        headings[currentActive].classList.remove('active');
        currentActive=0;
        headings[0].classList.add('active');
  }
   lastScrollTop = st <= 0 ? 0 : st; 
   });

The complete code snippet can be found below:-

let headings = Array.from(document.querySelectorAll('.animated-text'));
const sticky = document.querySelector('.sticky');
let lastScrollTop = 0;
let currentActive=0;
window.addEventListener('scroll', () => {
  const stickyDiv=sticky.getBoundingClientRect(); 
  let st = window.pageYOffset || document.documentElement.scrollTop; 
  if (st > lastScrollTop) {
     if (stickyDiv.top===0) {
     for(let i=currentActive;i<headings.length-1;i++)
        {
         const headingRect = headings[i].getBoundingClientRect();
         if(headingRect.top<=0){
                currentActive=i+1;
                headings[i].classList.remove('active');
                headings[i+1].classList.add('active');
         }
     }
  }
} else if (st < lastScrollTop) {
    if (stickyDiv.top===0) {
    for(let i=currentActive;i>0;i--)
    {
     const headingRect = headings[i].getBoundingClientRect();
     if(headingRect.top<=0){
            currentActive=i-1;
            headings[i].classList.remove('active');
            headings[i-1].classList.add('active');
     }
   }
  } }
  if(stickyDiv.top>0){
        headings[currentActive].classList.remove('active');
        currentActive=0;
        headings[0].classList.add('active');
  }
   lastScrollTop = st <= 0 ? 0 : st; 
   });
body {
  margin: 0
}

section {
  position: relative;
}

.sticky {
  padding-bottom: 150px;
  background: #2d232c;
  position: sticky;
  top: 0;
  overflow: hidden;
  height: auto;
  color: white;
}

.animated-text {
  opacity: 0;
  height: 0;
  overflow: hidden;
  transition: opacity 1s ease, height 1s ease, transform 1s ease;
  transform: translateY(0);
}

.animated-text.active {
  position:absolute;
  top:0;
  height: auto;
  opacity: 1;
  transform: translateY(-20px);
}

.hero, .end {
  height: 100px;
  background: white;
}
<section class='hero'>
  <p>Start</p>
</section>

<section class='sticky'>
  <div class='text-animations'>
    <h1 class='animated-text active'>Intro</h1>
    <h2 class='animated-text'>First</h2>
    <h2 class='animated-text'>Second</h2>
    <h2 class='animated-text'>Third</h2>
  </div>
  <p class='intro_content'>
     Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam Lorem ipsum dolor sit amer...
  </p>  
</section>

<section class='end'>
  <p>End<p>
</section>

If you need further clarification or explanation on any part of the logic, please don't hesitate to ask.

Answer №3

After experimenting, I found that using the translateY on scroll method yielded better results for me.

const items = $('.animated-text')
const carousel = $('.sticky')
const windowHeight = $(window).height()

// Set the original data-offset value for each carousel item.
items.each(function (i) {
  $(this).attr('data-offset', windowHeight * i)
})

$(window).scroll(function () {
  const scrollTop = $(window).scrollTop()
  const carouselTop = carousel.offset().top
  const carouselBottom = carouselTop + carousel.height()

  items.each(function () {
    const itemIndex = $(this).index()
    const itemOffset = parseInt($(this).attr('data-offset'), 10)
    const nextItemOffset = items.eq(itemIndex + 1).attr('data-offset')
    const offsetRatio = (scrollTop - itemOffset) / windowHeight

    // Calculate opacity and transform values based on the scroll position.
    let opacity = 1 - Math.abs(offsetRatio * 2)

    const translateY = -offsetRatio * 100

    // Only apply styles when the carousel is visible.
    if (scrollTop >= carouselTop && scrollTop < carouselBottom) {
      $(this).css({
        opacity: opacity,
        transform: `translateY(${translateY}%)`
      })

      // Check if the next item is visible, if so, hide the current item.
      if (nextItemOffset && scrollTop >= nextItemOffset) {
        $(this).css({ opacity: 0 })
      }
    } else if (scrollTop < carouselTop) {
      // When scrolling is above the carousel, reset the transformation for all items
      // But keep only the first item visible
      if ($(this).index() === 0) {
        $(this).css({
          opacity: 1,
          transform: 'translateY(0%)'
        })
      } else {
        $(this).css({
          opacity: 0,
          transform: 'translateY(0%)'
        })
      }
    }
  })
})
body {
  margin: 0
}

section {
  position: relative;
}

.sticky {
  height: 400vh;
}

.sticky__inner {
  background: #2d232c;
  overflow: hidden;
  align-items: center;
  color:#fff;
  display:flex;
  height: 100vh;
  justify-content:center;
  perspective:70rem;
  position:sticky;
  top: 0;
}

.text-animations {
  height: 60vh;
  position: relative;
  transform-origin: 50% 50%;
  transform-style: preserve-3d;
  width: 100vw;
  margin-top: -200px;
  left: 70px;
}

.animated-text {
  margin-top: 10vh; /* adjust this value as needed */
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  
  position: absolute;
  transform-origin: 50% 50%;
  transition: all 0.5s ease-in-out; /* Add this line */
  opacity: 0;

}

.animated-text :first-of-type {
  opacity: 1;
}

.hero, .end {
  height: 100px;
  background: white;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<section class='hero'>
  <p>Start</p>
</section>

<section class='sticky'>
 <div class='sticky__inner'>
  <div class='text-animations'>
    <h1 class='animated-text active'>Intro</h1>
    <h2 class='animated-text'>First</h2>
    <h2 class='animated-text'>Second</h2>
    <h2 class='animated-text'>Third</h2>
  </div>

  <p class='intro_content'>
     Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam
  </p>  
  </div>
</section>

<section class='end'>
  <p>End<p>
</section>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Attempting to position items within a container

Currently, I am in the process of creating a list of job opportunities to be displayed on a side bar of our careers page. Each job listing should include the job title along with an apply button enclosed within a rounded grey box that allows for some spaci ...

Tips for folding the center div downwards

In the code snippet below, there are three divs that should respond to window resizing by adjusting their layout. When the window becomes too small, the middle div should move down as illustrated in the accompanying image. <!DOCTYPE html> <html&g ...

Regular expression that identifies any string that does not conclude with .json

Currently, I am grappling with creating a JavaScript regex for a web application that identifies anything not ending in .json. It seems like a straightforward task, but it is proving to be quite challenging. Initially, my approach was using this regex pat ...

Uncovering the origin of a problematic included file

When using Sencha Touch, it is common to encounter issues related to missing files. In most cases, simply including the file resolves the issue. However, there are instances where the problem lies in a faulty include, requiring you to locate where the file ...

Do we really need to use the eval function in this situation?

Just wondering, is it reasonable to exclude the eval() function from this code? Specifically how <script> ... ... function addGeoJson (geoJsonPath, iconPath = "leaflet-2/images/marker-icon.png", iconSize = [30,50], popUpContent, ...

Guide to adding a play button overlay to an image with the use of javaScript

I've been exploring ways to incorporate an overlay play button onto an image, allowing me to click on it and play a video. My main concern is that most solutions require adding a distinct overlay play button directly to the source code. However, what ...

Does utilizing this.someFunction.bind(this) serve a purpose or is it duplicative

As I analyze someone's code, I stumbled upon the following snippet: this.device_name.changes().onValue(this.changeName.bind(this)) My interpretation so far is that onValue requires a callback function, which in this case is this.changeName.bind(this ...

A missing semicolon is encountered while compiling a React application

I am currently working on my app using Create React App and incorporating scss. I have used a vscode plugin to convert the scss to css, but I keep encountering an error when running npm run build. The error message reads as follows: [email protected ...

Retrieve the inner object within an object for tomorrow's date

I've searched for this information before but I can't find the answer - how do I access data in a data event to show data for the next date in the collection JadwalBooking? Data Schema: "keterangan" : "keterangan di rubah", "dataevent" : { " ...

Combining and integrating rows within a table

I have created a table with two columns labeled "Team" and "Members". My question is this: how can I add a new row when the plus icon in the Team column is clicked, and only add a row in the Members column when the plus icon in that column is clicked? & ...

Saving only the final item, a Json Array of Objects does not retain all of

I am facing an issue where I need to define an array of objects and store multiple objects within it. However, I can only add one object to the array (the last object) and cannot add more objects to it. Inside the Dataservice class getData(){ return this ...

Lazy loading images using the lazysizes plugin allows the browser to calculate the total page size without waiting for the images to

Having trouble with the lazysizes plugin and srcset on a fluid layout. The page keeps extending as you scroll instead of loading to the correct length You can see my attempt using the bootstrap framework in this CodePen demo After much research, it seems ...

Sending text content using AJAX

Recently, I've delved into the world of AJAX and JavaScript while working on a project in JSP. My goal is to submit text entered in a textarea using a JavaScript button. The first submit button worked fine initially, but upon posting, I ended up with ...

Transferring information from a component that includes slot content to the component within the slot content

This task may seem complex, but it's actually simpler than it sounds. I'm struggling with the correct terminology to enhance the title. I need to transfer data from a component that includes slot content to that slot content component. In partic ...

I seem to be facing a challenge with retrieving results in my app when using mongoose - what could be causing this issue

mongoose.connect('mongodb://localhost:27017/codealong'); When I attempt to connect to MongoDB and post data, the process is successful but I do not receive any results in my browser. Instead, all I see is an empty square bracket [ ]. These are ...

Troubleshooting Problem with Website Responsiveness on iPhones Using Bootstrap 5

I am currently experiencing a challenge involving the responsiveness of a website I am developing, particularly when it comes to iPhones. The site utilizes Bootstrap 5 and displays correctly on Android devices and in Chrome Dev Tools. However, upon testing ...

An error message stating that 'GAPI is undefined'

I've been encountering an issue while trying to integrate the Google Sheets API into my web application. The error message keeps indicating that the gapi library is not defined. I attempted to delay the server request using the ComponentDidMount lifec ...

What is the best way to merge 60 related jquery functions into a unified function?

I designed a webpage that serves as an extensive checklist. When you click on the edit button for a checklist item, a modal window opens up. This modal window contains a radio button to indicate whether any issues were found during the check. If "Yes" is s ...

How come the function is being triggered by my onclick button as soon as the page loads?

Currently, I am experiencing a challenge in my NodeJS project with Express. The issue lies with my EJS client side file when it comes to handling button click events. In my EJS file, I have imported a function from a JS file and can invoke it using <% ...

Using SQL to Insert Values into Select/Option

I am attempting to create a select form element with predetermined options. <select name="select1"> <option value="value1">Value 1</option> <option value="value2">Value 2</option> <option value="value3">Value 3 ...