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.

Progress Nav with IntersectionObserver

In Table of Contents with IntersectionObserver on CSS-Tricks, Chris Coyier talks about sticky table of contents on long pages whose active state updates as you scroll. When talking about those, you can not not mention the wonderful Progress Nav by Hakim El Hattab.


As Hakim’s demo from 2017 (!!) does not use IntersectionObserver — which was in it’s very early stages back then — Chris hinted that someone should make a version that uses it. Anders Grimsrud took up the challenge, and built it:

See the Pen
Hakim’s Progress Nav Concept using the Intersection Observer API
by Anders Grimsrud (@agrimsrud)
on CodePen.

Cool! 🙂

💡 Be sure to check Hakim’s “Building Better Interfaces” talk, which is full of inspiration to make your interfaces more delightful. Recommended stuff!

😊 I was very happy to see Chris including my 2020 Smooth Scrolling Sticky ScrollSpy Navigation in his post too.

Building a Side Navigation

Adam Argyle, writing for

In today’s GUI challenge we create a responsive, accessible slide out side navigation user experience using CSS and JS. The sidenav works on multiple browsers, screen sizes and input devices. Rad right? Follow as we employ grid, transforms, pseudo classes and a dollop of JavaScript to handle this UX.

On large screens the sidenav is shown next to the content, but on smaller screens the sidenav is placed off-screen and slides in when needed.

To open the sidenav he uses the :target pseudo-class, a trick I also used to create the Lightbox used in this Simple CSS Gallery.

What was me to me though is the use of min-content in combination with CSS Grid, something I hadn’t combined before.

#sidenav-container {
  display: grid;
  grid: [stack] 1fr / min-content [stack] 1fr;
  min-height: 100vh;

The snippet above creates a grid with one row and two columns. The 1st column has a width of min-content, and the 2nd column (labeled stack) a width of 1fr.

On large screen devices the sidebar (<aside> element) is placed in the 1st column, and the main content (<main> element) is placed in the stack grid-area (e.g. 1st row, 2nd column).

#sidenav-container > aside {
  grid-column: 1 / 2;

#sidenav-container > main {
  grid-area: stack;

The clue to this min-content width for the 1st column becomes clear when looking at the small screen layout. For that layout, the <aside> element is also position in the stack grid-area.

@media (max-width: 540px) {
  #sidenav-container > aside {
    grid-area: stack;

Because of that the 1st column will no longer contain anything, yielding a calculated value of 0 for min-content, thus making the column collapse entirely. As a result, the 1fr set on the stack grid-area will now be able to occupy the entire available width.

Add in some extra CSS to shift the sidebar off-screen and you have the result you see.

@media (max-width: 540px) {
  #sidenav-container > aside {
    grid-area: stack;

    visibility: hidden;
    transform: translateX(-110vw);
    transition: transform .6s ease-in-out;

  aside:target {
    visibility: visible;
    transform: translateX(0);

Building a sidenav component →
Sidenav Demo →

Don’t like to read? You can also check this video:

How Discord Implemented App-Wide Keyboard Navigation

When working on creating a complete keyboard navigation experience for Discord, using styling with :focus and outline, the folks at Discord ran into issues where the outline would not match the shape of actual element being rendered. Thinks like border-radius, overflow: hidden; on the container, padding got in the way. So they set out to find a solution.

After a lot of trial and error, we landed on a system which is built on two components: FocusRing for declaring where a ring should be placed, and FocusRingScope for declaring a root position for rings to be rendered.

Here’s an example showing how the FocusRing works:

function SearchBar() {
  const containerRef = React.useRef<HTMLDivElement>(null);
  const inputRef = React.useRef<HTMLInputElement>(null);
  return (
    <FocusRing focusTarget={inputRef} ringTarget={containerRef}>
      <div className={styles.container} ref={containerRef}>
        <input type="text" ref={inputRef} placeholder="Search" />
        <div className={styles.icon}>
          <ClearIcon />

The FocusRing will capture focussing of the contained input, but will render the ring around the entire div. To have a FocusRing behave like :focus-within and respond to any descendant being focussed, you can set the within prop.

The package can be installed using NPM:

npm i react-focus-rings

How Discord Implemented App-Wide Keyboard Navigation →
Browser Focus Ring Problems →
react-focus-rings (GitHub) →

Related: Not entirely coincidental the aforementioned React Spectrum by Adobe also comes with a FocusRing component.

Deep Dive into Page Lifecycle API

As the name suggests, the Page Lifecycle API exposes the web page lifecycle hooks to JavaScript. However, it isn’t an entirely new concept. Page Visibility API existed for some time, revealing some of the page visibility events to JavaScript.
However, if you happen to choose between these two, it’s worth mentioning some of the limitations of the Page Visibility API.

  • It only provides visible and hidden states of a web page.
  • It can not capture pages discarded by the operating system (Android, IOS, and the latest Windows systems can terminated background processes to preserve system resources).

Let’s take a look at the page lifecycle states exposed by the Page Lifecycle API.

To implement the Page Lifecycle API and help overcome browser inconsistencies, there’s PageLifecycle.js that will come in handy:

import lifecycle from '/path/to/page-lifecycle.mjs';

lifecycle.addEventListener('statechange', function(event) {
  console.log(event.oldState, event.newState);

Deep Dive into Page Lifecycle API →
The Page Lifecycle API →
PageLifecycle.js →

Avoiding tab styles for navigation

Adam Silver, who works at/with the fine folks at GOV.UK:

Tabs should only look like tabs if they behave like tabs otherwise it can be in disorienting and confusing for users.

Shown above is the old layout that featured the tabs (which are actually links, here). The new version still has the links in place at the same position, but they’ve styled them differently so that the look less like tabs.

Avoiding tab styles for navigation →

Using JavaScript’s closest() Method to Capture a “Click outside” an Element

In Practical Use Cases for JavaScript’s closest() Method, Andreas Remdt talks about some nice use cases that use Element.closest().

I especially like this example with a menu. Click on one of the links and it will show the menu which has the class menu-dropdown. Clicking outside said menu will close it. It’s that latter one that leverages Element.closest().

var menu = document.querySelector(".menu-dropdown");

function handleClick(evt) {
  // Only if a click on a dropdown trigger happens, either close or open it.
  // If a click happens somewhere outside .menu-dropdown, close it.
  if (!".menu-dropdown")) {

window.addEventListener("click", handleClick);

Here’s a pen with the result:

🔥 If you haven’t checked out Hakim El Hattab’s dynamically drawn hit areas for menus, as talked about in Building Better Interfaces, be sure to do so, as they’re amazing:

React Navigation 5.0 alpha – Rethinking Navigation as a Component-first API

Just announced at React Native EU is an alpha release of React Navigation 5.0, a navigator for use with React Native.

An exploration of a component-first API for React Navigation for building more dynamic navigation solutions.

  • Should play well with static type system
  • Navigation state should be contained in root component (helpful for stuff such as deep linking)
  • Component-first API

Instead of using constructs where you had to call the createStackNavigator function with an object …

const AppNavigator = createStackNavigator({
  Home: {
    screen: HomeScreen,

… there’s now a component that provides you with it:

const Stack = createStackNavigator();

function App() {
  return (
        <Stack.Screen name="Home" component={HomeScreen} />

React Navigation @next Documentation →
React Navigation @next Source (GitHub) →

pagemap, a mini map navigation for web pages

Many text editors nowadays sport a minimap on the right hand side of the screen. Pagemap is like that, but for webpages:

To use it, position a canvas element fixed on your screen, and tell pagemap which elements to include in the map:

<canvas id="map"></canvas>
#map {
    position: fixed;
    top: 0;
    right: 0;
    width: 200px;
    height: 100%;
    z-index: 100;
pagemap(document.querySelector('#map'), {
    viewport: null,
    styles: {
        'header,footer,section,article': rgba(0,0,0,0.08),
        'h1,a': rgba(0,0,0,0.10),
        'h2,h3,h4': rgba(0,0,0,0.08)
    back: rgba(0,0,0,0.02),
    view: rgba(0,0,0,0.05),
    drag: rgba(0,0,0,0.10),
    interval: null

Installation per npm/yarn

$ yarn add pagemap

pagemap – minimap for web pages →
pagemap source (GitHub) →