Smooth Scrolling Sticky ScrollSpy Navigation, with CSS fixed backgrounds

Davor Suljic created a fork of my Smooth Scrolling Sticky ScrollSpy Navigation that — instead of using IntersectionObserver — uses some background magic to highlight the active navigation items. The result is a “Pure CSS” implementation:

~

If you turn on an outline on one of the content sections it becomes clear how it exactly works:

  • The content sections also appear underneath the navigation
  • The content sections have background set, which has the exact same dimensions as their linked navigation item.
  • The backgrounds are positioned visually underneath their linked navigation item using background-position: fixed;

It’s a delicate combination, but I’m pretty excited that this actually works 🙂

Do note though:

  • You’ll need to add an extra wrapper whose width is limited around the section’s content, to prevent the content from appearing underneath the navigation.
  • Active state is a bit tricky:

    • Items high up the navigation list — such as Authentication — will become active pretty late, whereas items lower in the navigation bar — such as Expanders — will get their active state applied quite early. This is because the active state of nav items “starts” when the top of its linked container intersects with the nav item itself.
    • For small content-sections — such as Links – the nav item will become inactive too early, even before its linked section has left the viewport (and even when it still entirely in view). This is because the content itself is not that big and the nav item is pretty low in the navigation list.
  • You’re limited to background effects only. No making the nav items bold/italic, or changing their color.

HTML Table with Sticky Header Row and Sticky First Column

Nice work by Chris Coyier:

I tried this on a table before and would swear it didn’t work back then. Perhaps I did something wrong, because it’s quite tricky as Chris details:

The “trick” at play here is partially the position: sticky; usage, but moreso to me, how you have to handle overlapping elements. A table cell that is sticky needs to have a background, because otherwise we’ll see overlapping content. It also needs proper z-index handling so that when it sticks in place, it’ll be on top of what it is supposed to be on top of.

A table with both a sticky header and a sticky first column →

Sticky Photostack

Ooh I like this demo, making clever use of position: sticky;:

See the Pen Sticky Photostack by Bennett Feely (@bennettfeely) on CodePen.

There’s also some clever sizing going on in there to create the whitespace around the images, avoiding the need for a wrapper div per photo.


img {
    width: 100vmin;
    height: 100vmin;
    transform: scale(0.6) rotate(…);
}

Here’s a forked demo that also removes the figure, stripping it down to its core:

See the Pen Sticky Photostack (Forked) by Bramus (@bramus) on CodePen.

Sticky CSS Grid Items

Melanie Richards:

If you’ve ever tried to put a sticky item in a grid layout and watched the item scroll away with the rest of the content, you might have come to the conclusion that position: sticky doesn’t work with CSS Grid. Fear not! It is possible to get these two layout concepts working together. All you likely need is one more line of CSS.

The solution is to make it so that the item you want to stick does not stretch out vertically. You can do this by applying align-self: start; on said element, which is the same approach I took when implementing that Smooth Scrolling Sticky ScrollSpy Navigation earlier this year 🙂

Sticky CSS Grid Items →

The Slideout Footer

Chris Coyier, after seeing the footer on a site called The Markup:

The footer is clever, in how it appears to slide out from underneath the content as you scroll to the bottom of the page. Let’s make it!

Scroll to the very bottom in this pen to see the footer appear from underneath the content:

The trick is to have the footer:

  1. Stick to the bottom (using position: sticky;)
  2. Be the last item on the page

That way, as there’s no content below the footer, it can only keep on sticking to the bottom 🙂

The Slideout Footer | CSS-Tricks →

Smooth Scrolling Sticky ScrollSpy Navigation

Yesterday evening I was working on a documentation page. The page layout is quite classic, as it consists of a content pane on the left and a sidebar navigation on the right. Looking for a way to make the page less dull I decided to add a few small things to it:

  1. Smooth Scrolling when clicking internal links
  2. A Sticky Navigation, so that the sidebar navigation always stays in view
  3. A “ScrollSpy” to update the active state of the navigation

These three additions make the page more delightful, and best of all is: they’re really easy to implement!

In this post I’ll lay out the details.

~

The result

First things first, here’s a recording of the end result so that you get an idea of what I’m talking about:

~

The Markup

The markup is really basic:

  • A main element surrounds our content div and nav.
  • Each piece of content is wrapped in a section which gets an id attribute. The sidebar navigation then links to its id
<main>
	<div>
		<h1>Smooth Scrolling Sticky ScrollSpy Navigation</h1>
		<section id="introduction">
			<h2>Introduction</h2>
			<p>…</p>
		</section>
		<section id="request-response">
			<h2>Request &amp; Response</h2>
			<p>…</p>
		</section>
		<section id="authentication">
			<h2>Authentication</h2>
			<p>…</p>
		</section>
		…
		<section id="filters">
			<h2>Filters</h2>
			<p>…</p>
		</section>
	</div>
	<nav class="section-nav">
		<ol>
			<li><a href="#introduction">Introduction</a></li>
			<li><a href="#request-response">Request &amp; Response</a></li>
			<li><a href="#authentication">Authentication</a></li>
		…
			<li class=""><a href="#filters">Filters</a></li>
		</ol>
	</nav>
</main>

Sprinkle some CSS on top to lay everything out – using CSS Grid here – and you have a fully working – albeit dull – page:

See the Pen
Smooth Scrolling Sticky ScrollSpy Navigation (base layer)
by Bramus (@bramus)
on CodePen.

~

1. Smooth Scrolling

Enabling smooth scrolling is really easy, it you can enable it using a single line of CSS:

html {
	scroll-behavior: smooth;
}

😱 Yes, that’s it!

In the demo embedded below, click any of the links in the nav and see how smooth it scrolls:

See the Pen
Smooth Scrolling Sticky ScrollSpy Navigation (base layer)
by Bramus (@bramus)
on CodePen.

For browsers that don’t support this you could add this JS fallback:

// Smooth scrolling for browsers that don't support CSS smooth scrolling
if (window.getComputedStyle(document.documentElement).scrollBehavior !== 'smooth') {
    document.querySelectorAll('a[href^="#"]').forEach(internalLink => {
        const targetElement = document.querySelector(internalLink.getAttribute('href'));
        if (targetElement) {
            internalLink.addEventListener('click', (e) => {
                targetElement.scrollIntoView({
                    behavior: 'smooth',
                });
                e.preventDefault();
            });
        }
    });
}

However, browser’s that don’t support scroll-behavior: smooth; also don’t support behavior: "smooth" for Element#scrollIntoView, so there’s not real advantage to adding this JS fallback.

~

2. Sticky Navigation

To make the navigation stay in place as you scroll we can rely on position: sticky;. As with Smooth Scrolling, this is a really simple CSS addition:

main > nav {
	position: sticky;
	top: 2rem;
	align-self: start;
}

💁‍♂️ Since we’re using CSS Grid to lay out the children of <main>, adding align-self: start; to <nav> is an important one here. If we would omit it, the nav element would be as high as the enclosing main element. If that were the case, then nav would never be able to stick.

In the demo embedded below, click any of the links in the nav and see how the nav now also stays in view while the rest of the page scrolls:

See the Pen
Smooth Scrolling Sticky ScrollSpy Navigation (base layer + smooth scrolling + sticky nav)
by Bramus (@bramus)
on CodePen.

~

👋 Like what you see so far? Follow @bramusblog on Twitter or subscribe to the RSS feed to stay up-to-date.

~

3. ScrollSpy with IntersectionObserver

Thanks to the almighty IntersectionObserver we can implement a ScrollSpy. Basically we use it to watch all section["id"] elements. If they are intersecting, we add the .active class to any link that links to it. For styling purposes we don’t add .active to the link itself, but to its surrounding li element.

In code, that becomes this little snippet:

window.addEventListener('DOMContentLoaded', () => {

	const observer = new IntersectionObserver(entries => {
		entries.forEach(entry => {
			const id = entry.target.getAttribute('id');
			if (entry.intersectionRatio > 0) {
				document.querySelector(`nav li a[href="#${id}"]`).parentElement.classList.add('active');
			} else {
				document.querySelector(`nav li a[href="#${id}"]`).parentElement.classList.remove('active');
			}
		});
	});

	// Track all sections that have an `id` applied
	document.querySelectorAll('section[id]').forEach((section) => {
		observer.observe(section);
	});
	
});

💡 To make the transition to and from .active not too abrupt, add a little blob of CSS to ease things:

.section-nav a {
    transition: all 100ms ease-in-out;
}

~

Complete Demo

Putting everything together, we end up with this:

See the Pen
Smooth Scrolling Sticky ScrollSpy Navigation
by Bramus (@bramus)
on CodePen.

Delightful, no? 😊

~

💡 If you’re also looking for more inspiration to make your interfaces more delightful, be sure to check Hakim El Hattab’s “Building Better Interfaces” talk. Recommended stuff!

Did this help you out? Like what you see?
Thank me with a coffee.

I don't do this for profit but a small one-time donation would surely put a smile on my face. Thanks!

☕️ Buy me a Coffee (€3)

To stay in the loop you can follow @bramus or follow @bramusblog on Twitter.

Combining position: sticky; with overflow: scroll;

Figure: position: sticky; and overflow: scroll;, a quirky combination … but it can be fixed!

Dannie Vinther:

Say we want an overflowing table of columns and rows with sticky headings on a page. We want the headings to stick while scrolling on the document window, and we want to be able to scroll horizontally within the overflowing container.

When working with overflows you might find that your sticky element isn’t so sticky after all, which may cause some frustration. The browser doesn’t seem to be respecting position: sticky; once we add overflow to the mix.

The solution is to use two scroll containers and sync up their scrolling position using a tad of JavaScript:

See the Pen Position Sticky Table with overflow by Dannie Vinther (@dannievinther) on CodePen.

Let’s hope this quirky combination gets fixed in CSS itself. Until then we’ll need this JS-based solution.

position: stuck; — and a Way to Fix It →
syncscroll (GitHub) →

❓ Not familiar with position: sticky;? Check out this introduction then.

Sticky Events – Events for position: sticky;

Sticky Events is a library that can listen for events on elements that have position: sticky; applied. It’s an abstraction built on top of the IntersectionObserver, and provides one with three types of events:

  • StickyEvent.CHANGE: Fired when an element becomes stuck or unstuck
  • StickyEvent.STUCK: Fired only when an element becomes stuck
  • StickyEvent.UNSTUCK: Fired only when an element becomes unstuck

Usage is quite simple: set up it once, and then add event listeners to the elements:

import { observeStickyEvents, StickyEvent } from "sticky-events";

// Add listeners to all `.sticky-events` elements on the page
observeStickyEvents();

// Events are dispatched on elements with the `.sticky-events` class
const stickies = Array.from(document.querySelectorAll('.sticky-events'));

stickies.forEach((sticky) => {
  sticky.addEventListener(StickyEvent.CHANGE, (event) => {
    sticky.classList.toggle('bg-dark', event.detail.isSticky);
  });

  sticky.addEventListener(StickyEvent.STUCK, (event) => {
    console.log('stuck');
  });

  sticky.addEventListener(StickyEvent.UNSTUCK, (event) => {
    console.log('unstuck');
  });
});

Sticky Events – Events for position: sticky;

Fixed-sticky: a CSS position:sticky; polyfill

CSS position: sticky; is really in its infancy in terms of browser support. In stock browsers, it is currently only available in iOS 6. In Chrome it is locked behind a chrome://flags setting. Fixed-sticky is a polyfill to enabling this in browsers that don’t support it yet.

position: sticky; is one very handy addition to CSS, paving the cowpaths.

Fixed-Sticky (Github) →
Fixed-Sticky Demo →

Related: the aforementioned jQuery Sticky Objects and jQuery Stick’em. Whilst they look similar in result, position: sticky; is the way to go.