Petr Cibulka
Petr Cibulka

Reputation: 2513

Position sticky: scrollable, when longer than viewport

When element with position: sticky is "stuck" and is longer than viewport, you can only see its content after you scroll to the bottom of container.

It would be cool, if the "stuck" element scrolled with the document and stopped, once it reaches its bottom edge. If user scrolled back, the same thing would happen again, but in reverse.

Examples

TLDR; There is a library (StickyKit) that does what I'm looking for, but performs badly with new async scrolling.

JSFiddle with StickyKit - https://jsfiddle.net/cibulka/4nd3b0tt/ - (this does what I'm describing, but performs poorly, see below)

JSFiddle with native position: sticky - https://jsfiddle.net/cibulka/np6afe9c/1/ - https://jsfiddle.net/cibulka/pxz6e0qv/ - (this does not)

Background

I was a happy user of StickyKit for a long time. Unfortunately, it does not work very well with asynchronous scrolling, which is employed by more and more browsers to improve performance. With the new Firefox Quantum (57), for example, StickyKit is pretty much unusable.

I've created an issue in StickyKit Github, but package seems to be abandonned by the author: https://github.com/leafo/sticky-kit/issues/252

Because of that, I am forced to deprecate StickyKit and move to native position:sticky (polyfilled with StickyFill). Unfortunately there is a couple of things position:sticky can't do and this is one of them.

There is also another issue I'm having with position:sticky: Position sticky: overlay

What I'm looking for

A recommendation, basically, how to approach this issue. I'm up for using different JS/jQuery library, writing my own code or use some quirky CSS hack to hack position:sticky functionality.

Upvotes: 35

Views: 12027

Answers (4)

PinkiNice
PinkiNice

Reputation: 1493

There is a library called Sticky Sidebar which is made exactly for solving that problem.

Demo in codepen without sticky sidebar

Demo in codepen with sticky sidebar

  1. Include the library sticky-sidebar.min.js on your page

  2. Your DOM should look roughly like this:

    <div class="main-content">
        <div class="sidebar">
            <div class="sidebar__inner">
                <!-- Content goes here -->
            </div>
        </div>
        <div class="content">
            <!-- Content goes here -->
        </div>
    </div>
    
  3. And then you can initialize like this

    var sidebar = new StickySidebar('.sidebar', {
      topSpacing: 16,
      bottomSpacing: 16,
      containerSelector: '.content',
      innerWrapperSelector: '.sidebar__inner'
    });
    

Upvotes: 6

Austin
Austin

Reputation: 115

I've taken jnns answer and updated it so that it is smooth between scrolls like sticky kit may have been. The only issue is that it required a magic number for a containing div s.t. the container maintains it's size while the div is absolutely positioned - this might be solved in your code by a css variable.

window.onscroll = function (e) {
  if (window.scrollY < this.prevScrollY) {
    // Track position state of nav
    // 1 == stuck to top
    // 0 == absolute positioning
    // -1 == stuck to bottom 
    this.stick_pos = scrollUpwards(this.stick_pos);
  } else {
    this.stick_pos = scrollDownwards(this.stick_pos);
  }
  this.prevScrollY = window.scrollY; 
}

function scrollUpwards(stick_pos) {
  // If the element is already stuck to the top then we are fine
  if(stick_pos === 1) return stick_pos;
  // Figure out where the new window will be after scroll
  let aside = $("aside").get(0);
  let aboveAside = aside.getBoundingClientRect().top > 0;
  // If we are going above the element then we know we must stick
  // it to the top
  if (aboveAside){
    $("aside").css("position", "sticky")
      .css("top", 0)
      .css("bottom", '')
      .css("align-self", "flex-start");
    return 1;
  }
  // If it will still be below the top of the element, then we
  // must absolutely position it to its current position - if it already is absolutely positioned then we do nothing
  if (stick_pos == 0) return stick_pos;
  
  // Undo the stick to the bottom
  // First get the current position
  $("aside")
    .css("top", aside.offsetTop)
    .css("position", "absolute")
    .css("bottom", '')
    .css("align-self", "");
  return 0;
}

function scrollDownwards(stick_pos) {
  /*
  let aside = $("aside").get(0);
  let aboveAside = aside.offsetTop >= window.scrollY;
  let browser_bottom = window.scrollY + window.innerHeight;
  let aside_bottom = aside.offsetTop + aside.offsetHeight;
  let belowAside = browser_bottom >= aside_bottom;
  if (aboveAside) {
    //console.log("stick to bottom");
    $("aside").css("top", ''); 
    $("aside").css("bottom", 0); 
    $("aside").css("align-self", "flex-end");
  }
  */
  // If the element is already stuck to the bottom then we are fine
  if(stick_pos === -1) return stick_pos;
  // Figure out where the new window will be after scroll
  let aside = $("aside").get(0);
  let browser_bottom = window.innerHeight;
  let aside_bottom = aside.getBoundingClientRect().top + aside.offsetHeight;
  let belowAside = browser_bottom > aside_bottom;
  // If we are going below the element then we know we must stick
  // it to the bottom.
  if (belowAside){
    $("aside").css("position", "sticky")
      .css("top", '')
      .css("bottom", 0)
      .css("align-self", "flex-end");
    return -1;
  }
  // If it will still be above the bottom of the element, then we
  // must absolutely position it to its current position - if it already is absolutely positioned then we do nothing
  if (stick_pos == 0) return stick_pos;
  
  // Undo the stick to the top
  // First get the current position
  // $("aside").css("position", "absolute")
  // .css("top", aside.offsetTop);
  $("aside")
    .css("top", aside.offsetTop)
    .css("position", "absolute")
    .css("bottom", '')
    .css("align-self", "");
  return 0;
}
div#section {
  /* begin: irrelevant styling */
  margin: 5em auto;
  padding: 0.625rem;
  max-width: 300px;
  font-family: sans-serif;
  font-size: 18px;
  line-height: 1.5em;
  text-align: justify;
  background-color: #dbe4ee;
  /* end: irrelevant styling */
  display: flex;
  justify-content: space-around;
}
div#section div#nav-container {
  position: relative;
  display: flex;
  min-width: 2em;
}
div#section div#nav-container aside {
  position: sticky;
  align-self: flex-start;
  /* begin: irrelevant styling */
  background-color: #81a4cd;
  color: white;
  text-align: center;
  width: 2em;
}
div#section div#nav-container aside div {
  padding: 0 .3em;
}
div#section article {
  margin-left: 0.5em;
}
div#section article p {
  margin: 0;
}
div#section article p + p {
  margin-top: 1.5em;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id='section'>
  <div id='nav-container'>
  <aside>
    <div>A</div>
    <div>B</div>
    <div>C</div>
    <div>D</div>
    <div>E</div>
    <div>F</div>
    <div>G</div>
    <div>H</div>
    <div>I</div>
    <div>J</div>
    <div>K</div>
    <div>L</div>
    <div>M</div>
    <div>N</div>
    <div>O</div>
    <div>P</div>
    <div>Q</div>
    <div>R</div>
    <div>S</div>
    <div>T</div>
    <div>U</div>
    <div>V</div>
    <div>W</div>
    <div>X</div>
    <div>Y</div>
    <div>Z</div>
  </aside>
  </div>
  <article>
    <p>Perferendis ut iusto voluptatem ex temporibus aut autem amet. Sit vero in soluta. Est officia asperiores tenetur vel quam nostrum eum facere. Sed totam quasi libero at facilis doloremque. Non aut velit odio. Tempora dolore sunt recusandae sed quia
      sunt.</p>

    <p>Voluptatem optio asperiores dolorem voluptatem. Ipsa alias perspiciatis doloribus est nisi ut. Fuga aut et vitae consequatur dolor corrupti aut minima.</p>

    <p>Facilis et ut eligendi. Excepturi labore asperiores vero. Perferendis porro sunt molestiae. In sit dolorem eum esse sit inventore est. Atque perspiciatis commodi nihil.</p>

    <p>Consequatur ipsa id repellendus voluptatem perspiciatis temporibus. Praesentium eveniet nemo laudantium inventore similique impedit nihil esse. Maiores iste commodi molestiae quas odit nihil ex corrupti. Illum id amet non vero.</p>

    <p>Voluptas soluta itaque et. Aperiam quasi sint eos ullam. Assumenda facilis omnis alias numquam. Odio quia esse vel et minima soluta architecto. Qui saepe consequatur aut rerum. Et et aut voluptatibus inventore.</p>

    <p>Perferendis ut iusto voluptatem ex temporibus aut autem amet. Sit vero in soluta. Est officia asperiores tenetur vel quam nostrum eum facere. Sed totam quasi libero at facilis doloremque. Non aut velit odio. Tempora dolore sunt recusandae sed quia sunt.</p>

    <p>Voluptatem optio asperiores dolorem voluptatem. Ipsa alias perspiciatis doloribus est nisi ut. Fuga aut et vitae consequatur dolor corrupti aut minima.</p>

    <p>Facilis et ut eligendi. Excepturi labore asperiores vero. Perferendis porro sunt molestiae. In sit dolorem eum esse sit inventore est. Atque perspiciatis commodi nihil.</p>

    <p>Consequatur ipsa id repellendus voluptatem perspiciatis temporibus. Praesentium eveniet nemo laudantium inventore similique impedit nihil esse. Maiores iste commodi molestiae quas odit nihil ex corrupti. Illum id amet non vero.</p>

    <p>Voluptas soluta itaque et. Aperiam quasi sint eos ullam. Assumenda facilis omnis alias numquam. Odio quia esse vel et minima soluta architecto. Qui saepe consequatur aut rerum. Et et aut voluptatibus inventore.</p>
</div>

Upvotes: 7

jnns
jnns

Reputation: 5634

You can try to switch the anchor and position of the sticky element from top to bottom with jQuery depending on the scrolling direction and still use CSS's native position: sticky.

I made it work in this codepen but didn't have the time to smooth out the jumpiness when directions are changed. But maybe this is enough for you.

// Use display: flex on the container

window.onscroll = function (e) {
  if (window.scrollY < this.prevScrollY) {
    scrollUpwards();
  } else {
    scrollDownwards();
  }
  this.prevScrollY = window.scrollY; 
}

function scrollUpwards() {
  $("aside").css("top", 0); 
  $("aside").css("bottom", '');
  $("aside").css("align-self", "flex-start");

}

function scrollDownwards() {
  $("aside").css("top", ''); 
  $("aside").css("bottom", 0); 
  $("aside").css("align-self", "flex-end");
}

Upvotes: 0

Prashant
Prashant

Reputation: 8040

Did you look into Scrollama

It uses the new Intersection Observer Web API which is basically browser telling JS when a certain element appears inside a viewport, without JS having to listen to scroll events. Therefore, useful for implementing position: sticky like behavior in JS in a performant way.

Upvotes: 0

Related Questions