2

I'm trying to create a progress bar that shows how much of a certain element the user still has left to view. Here are some details:

  • .postProgressBar appears by default under .postHeroImage
  • When the user scrolls, I want the .postProgressBar to slowly fill up based on how much of the .spacer element there is left to scroll to.
  • When the .postProgressBar hits the bottom of my header, I want it to become fixed to the bottom of the header (and to unfix when .postHeroImage is in view again).

See my current approach:

$(function() {

gsap.registerPlugin(ScrollTrigger);

  $(window).scroll(function() {
    var scroll = $(window).scrollTop();
    if (scroll >= 1) {
      $(".header").addClass("fixed");
    } else {
      $(".header").removeClass("fixed");
    }
  });
  
    var action = gsap.set('.postProgressBar', { position:'fixed', paused:true});

  gsap.to('progress', {
    value: 100,
    ease: 'none',
    scrollTrigger: {
      trigger: "#startProgressBar",
      scrub: 0.3,
      markers:true,
      onEnter: () => action.play(),
      onLeave: () => action.reverse(),
      onLeaveBack: () => action.reverse(),
      onEnterBack: () => action.reverse(),
    }
  });

});
body {
  background-color: lightblue;
  --white: #FFFFFF;
  --grey: #002A54;
  --purple: #5D209F;
}

.header {
  position: absolute;
  top: 0;
  width: 100%;
  padding: 20px 15px;
  z-index: 9999;
  background-color: var(--white);
}
.header.fixed {
  position: fixed;
  background-color: var(--white);
  border-bottom: 1px solid var(--grey);
}

.postHeroImage {
  padding: 134px 0 0 0;
  margin-bottom: 105px;
  position: relative;
}
.postHeroImage__bg {
  background-size: cover;
  background-repeat: no-repeat;
  width: 100%;
  min-height: 400px;
}

progress {
  position: absolute;
  bottom: -15px;
  left: 0;
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 15px;
  border: none;
  background: transparent;
  z-index: 9999;
}

progress::-webkit-progress-bar {
  background: transparent;
}

progress::-webkit-progress-value {
  background: var(--purple);
  background-attachment: fixed;
}

progress::-moz-progress-bar {
  background: var(--purple);
  background-attachment: fixed;
}

.spacer {
  height: 1000vh;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.0/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.0/ScrollTrigger.min.js"></script>

<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">


<body>

  <header class="header">Header</header>

  <section class="postHeroImage" id="startProgressBar">

    <progress class="postProgressBar" max="100" value="0"></progress>

    <div class="container">
      <div class="row">
        <div class="col-12">
          <div class="postHeroImage__bg" style="background-image: url( 'https://picsum.photos/200/300' );" loading="lazy"></div>
        </div>
      </div>
    </div>
  </section>

  <div class="spacer">lorum ipsum</div>

</body>

Current issues:

  1. The .postProgressBar doesn't become fixed (can't see fixed inline style in inspect mode)
  2. The .postProgressBar is showing progress that isn't accurate based on the amount of .spacer there is left to scroll.
Freddy
  • 683
  • 4
  • 35
  • 114

2 Answers2

1

Instead of the library that you use, you could simply use the HTML tag <progress> (your library is using the same).

Here is a simple way of how you can do it:

const scrollHeight = Math.max(
  document.body.scrollHeight, document.documentElement.scrollHeight,
  document.body.offsetHeight, document.documentElement.offsetHeight,
  document.body.clientHeight, document.documentElement.clientHeight
);

window.addEventListener('scroll', (e) => document.querySelector('progress').value = window.pageYOffset / (scrollHeight - window.innerHeight) * 100)
html, body {
  padding: 0;
  margin: 0;
  height: 2500px;
}

progress {
  position: fixed;
  border-radius: 0;
  width: 100%;
  height: 22px;
}

progress::-webkit-progress-value {
  background-color: #5D209F;
}
<progress value="0" max="100"></progress>

Every time the user scrolls the page, the progress gets a value, which is current height position / (document's height - active screen height) * 100.

Reza Saadati
  • 5,018
  • 4
  • 27
  • 64
1
  1. Position sticky is very useful here. You can use it for header as well as for the progress bar. With no effort it'll keep the elements fixed to top.
  2. For accurate position you need to use start and end properties of the scrollTrigger. This will tell the gsap when to start animating and when to end. For end we need to add 64px, the height of the header, to the scrolling cos our container is 64px below from top of the viewport.
    Demo:

$(function() {
  //read the css variable
  let bodyStyles = window.getComputedStyle(document.body);
  let headerHeight = bodyStyles.getPropertyValue('--header-height'); 

  gsap.registerPlugin(ScrollTrigger);
  gsap.to('progress', {
    value: 100,
    ease: 'none',
    scrollTrigger: {
      trigger: "#startProgressBar",
      scrub: 0.3,
      start: 'start 0px',
      end: 'bottom' + headerHeight,
      markers: true,
    }
  });
});
body {
  background-color: lightblue;
  --white: wheat;
  --grey: #002A54;
  --purple: #5D209F;
  /* 40px padding top and bottom + 24px line height*/
  --header-height: 64px;
}

.header {
  /* position: absolute;*/
  position: sticky;
  top: 0;
  width: 100%;
  padding: 20px 15px;
  z-index: 9999;
  background-color: var(--white);
}

.postHeroImage {
  padding: 134px 0 0 0;
  margin-bottom: 105px;
  position: relative;
}

.postHeroImage__bg {
  background-size: cover;
  background-repeat: no-repeat;
  width: 100%;
  min-height: 400px;
}

progress {
  position: sticky;
  top: var(--header-height);
  left: 0;
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 15px;
  border: none;
  background: transparent;
  /*z-index: 9999;*/
}

progress::-webkit-progress-bar {
  background: transparent;
}

progress::-webkit-progress-value {
  background: var(--purple);
  background-attachment: fixed;
}

progress::-moz-progress-bar {
  background: var(--purple);
  background-attachment: fixed;
}

.spacer {
  height: 1000vh;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.0/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.0/ScrollTrigger.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">


<header class="header">Header</header>
<section class="postHeroImage" id="startProgressBar">
  <progress class="postProgressBar" max="100" value="0"></progress>
  <div class="container">
    <div class="row">
      <div class="col-12">
        <div class="postHeroImage__bg" style="background-image: url( 'https://picsum.photos/id/705/300/200' );" loading="lazy"></div>
      </div>
    </div>
  </div>
</section>

<div class="spacer">lorum ipsum</div>

You can play with start and end properties. If you want to track progress on the content only then change trigger to '.container' .

Solution without sticky Here I've used position fixed:

$(function() {
  //read the css variable
  let bodyStyles = window.getComputedStyle(document.body);
  let headerHeight = bodyStyles.getPropertyValue('--header-height');

  gsap.registerPlugin(ScrollTrigger);

  var action = gsap.set('.postProgressBar', {
    position: 'fixed',
    paused: true
  });

  ScrollTrigger.create({
    trigger: '.postProgressBar',
    start: 'top 64px',
    onEnter: () => action.play(),
    onLeaveBack: () => action.reverse(),
  });

  gsap.to('progress', {
    value: 100,
    ease: 'none',
    scrollTrigger: {
      trigger: "#mainContent",
      scrub: 0.3,
      end: 'bottom 110%',
      markers: false,
    }
  });
});
* {
  margin: 0;
  padding: 0;
}

body {
  background-color: lightblue;
  --white: wheat;
  --grey: #002A54;
  --purple: #5D209F;
  /* 40px padding top and bottom + 24px line height*/
  --header-height: 64px;
  position: relative;
  height:100%
}

.header {
  position: fixed;
  top: 0;
  width: 100%;
  padding: 20px 15px;
  z-index: 9999;
  background-color: var(--white);
}

.postHeroImage {
  padding: 134px 0 0 0;
  margin-bottom: 105px;
  position: relative;
  margin-top: var(--header-height);
}

.postHeroImage__bg {
  background-size: cover;
  background-repeat: no-repeat;
  width: 100%;
  min-height: 400px;
}

progress {
  position: static;
  top: var(--header-height);
  left: 0px;
  -webkit-appearance: none;
  appearance: none;
  width: 100%;
  height: 15px;
  border: none;
  background: transparent;
  /*z-index: 9999;*/
}

progress::-webkit-progress-bar {
  background: transparent;
}

progress::-webkit-progress-value {
  background: var(--purple);
  background-attachment: fixed;
}

progress::-moz-progress-bar {
  background: var(--purple);
  background-attachment: fixed;
}

.spacer {
  height: 1000vh;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.0/gsap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.9.0/ScrollTrigger.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet">


<header class="header">Header</header>
<section class="postHeroImage" id="startProgressBar">
  <div class="container">
    <div class="row">
      <div class="col-12">
        <div class="postHeroImage__bg" style="background-image: url( 'https://picsum.photos/id/705/300/200' );" loading="lazy"></div>
      </div>
    </div>
  </div>
  <progress class="postProgressBar" max="100" value="0"></progress>
</section>

<div id="mainContent" class="spacer">lorum ipsum</div>
the Hutt
  • 16,980
  • 2
  • 14
  • 44
  • Hi, is there a way to achieve this without `sticky`? I say this because `sticky` does not work if any parent containers have `overflow: hidden`, which is the case in my build. Additionally, I need the progress bar to show below the image by default and then, when it hits the bottom of the `header`, then I want it to be fixed below the `header` – Freddy Dec 28 '21 at 19:13
  • I've added solution without sticky. Using original position fixed action. – the Hutt Dec 31 '21 at 13:25
  • Hi again, I'm trying to get the progress bar to indicate how much of the page is left to scroll. For example, in your demo, the the scroll bar is already complete by the time the `.spacer` div is hit. There's still a lot more of the page to scroll, which the bar should indicate. Also, the progress bar is hidden for a bit then gets fixed. Is it possible to have it fix as it's scrolling (smoothly)? See this website for a demo: https://www.magneticcreative.com/journal/hubl-templating-hubspot-cms/ – Freddy Dec 31 '21 at 22:11
  • I thought progress is for the image. I've updated the second snippet in the answer. – the Hutt Jan 01 '22 at 10:29