HorusKol

Relative and absolute scrolling blues

April 13, 2022

The other day I wanted to place an element into a scrollable container that would stay anchored near the bottom of the container as the user scrolled the rest of the content.

I thought that you could do this with absolute positioning:

<style>
  .container {
    position: relative;
    height: 12rem;
    overflow: auto;
  }

  .anchored {
    position: absolute;
    bottom: 1rem;
    height: 2rem;
    width: 100%;
    background-color: #4b5563;
  }
</style>

<div>
  <div class="container">
    <div class="anchored"></div>

    <p></p>
  </div>
</div>

As you can see - the "anchored" bar still moves as the user scrolls, even though the bar starts where we want it to.

My mental model was wrong - I was thinking that the anchored item would be positioned relative to the bounding box around the scrollable content. It isn't. The item starts positioned relative to the bounding box - but it will keep its position relative to the parent (which scrolls within the bounding box), so it scrolls, too.

To make better sense of this, think about what would happen if you set overflow to visible instead of auto. The absolutely positioned element in the container will be anchored to the container - but the content will continue on down. As you scroll the whole page down, the absolutely positioned element will keep its place relative to the rest of the content.

Contain yourself

I reached out to the Perth Fenders community, which includes some really awesome developers and designers. One solution from Pete Barr involves using some additional containers:

<style>
  .container {
    position: relative;
    height: 12rem;
  }

  .anchored {
    position: absolute;
    z-index: 1;
    bottom: 1rem;
    height: 2rem;
    width: 100%;
    background-color: #4b5563;
  }

  .content {
    position: absolute;
    overflow-y: auto;
    height: 100%;
    width: 100%;
  }
</style>

<div>
  <div class="container">
    <div class="anchored"></div>

    <div class="content">
      <p></p>
    </div>
  </div>
</div>

The only problem here is that the scrollbar is obscured slightly by the anchored element - though this could probably be resolved with a bit more refinement.

Getting sticky with it

I also had a try at using sticky positioning. This has been around for a few years, but only received broad support in early 2021 (Chrome and Edge were the holdouts).

If you have something with position: sticky and top: 0, it will appear as you scroll up with the rest of the content, but as you scroll past the sticky element it will now sit at the top.

Similarly, bottom: 0 will have it sit at the bottom until you scroll past where it would be in the flow.

<style>
  .container {
    position: relative;
    height: 12rem;
    overflow: auto;
  }

  .anchored {
    position: sticky;
    top: calc(100% - 3rem);
    height: 2rem;
    width: 100%;
    background-color: #4b5563;
  }
</style>

<div>
  <div class="container">
    <div class="anchored"></div>

    <p></p>
  </div>
</div>

I anchor the element from the top of the container with top: calc(100% - 3rem); to ensure it stays where I want it. Unfortunately, I couldn't position it relative to the bottom, because there might not be enough other content in the container to make sure it always stays anchored down there.

But this solution itself creates a problem - there is now a gap at the top of the content which was the same size as the anchored item.

We can fix this with a height fix:

<style>
  .container {
    position: relative;
    height: 12rem;
    overflow: auto;
  }

  .anchored {
    position: sticky;
    top: calc(100% - 3rem);
    height: 0;
    overflow: visible;
  }

  .anchored > div {
    height: 2rem;
    width: 100%;
    background-color: #4b5563;
  }
</style>

<div>
  <div class="container">
    <div class="anchored">
      <div></div>
    </div>

    <p></p>
  </div>
</div>

Refinement

The above code works for when there's one line of text in the anchored element - but what about when we don't know how much text will be in there? It would just overflow down out of the viewable area.

Absolute position to the rescue!

Some content
And some more
Again

.anchored {
  position: sticky;
  top: 100%;
  height: 0;
  overflow: visible;
}

#example-05 .anchored > div {
  position: absolute;
  bottom: 1rem;
  width: 100%;
  background-color: #4b5563;
  color: white;
  text-align: center;
}

So, our zero-height anchored element is stuck to the bottom of the viewable area - and its contents are absolutely positioned relative to the anchored element, so it grows up the page and not down.

Side-scrolling adventure

As I was about to write all this, I saw a tweet by Dries Vints describing a similar problem, but in the horizontal axis. He was looking to have a button or toolbar sit over on the top right of a preformatted code example.

<style>
.container {
  position: relative;
  height: 12rem;
  overflow: auto;
}

.toolbar {
  position: sticky;
  top: 0;
  left: 100%;
  height: 0;
  width: 0;
  overflow: visible;
}

.toolbar > div {
  position: absolute;
  top: 1rem;
  right: 1rem;
  padding: 0.5rem;
  background-color: #4b5563;
  color: white;
}
</style>

<pre class="container"><code><div class="toolbar"><div><button>Copy</button></div></div>.toolbar > div {
  position: absolute;
  top: 1rem;
  right: 1rem;
  padding: 0.5rem;
  background-color: #4b5563;
  color: white;
}</code></pre>

Because the <pre> element uses preformatted space, we need to insert the toolbar on a single line with not whitespace, otherwise the sample code will have a margin above and below.

Other than that, we can see that we've stuck the toolbar to the top right of the scrollable container, and in a way that would allow use to easily insert multiple buttons without having to change the positioning.