25% off ProSNIPC25OFF

Smooth-Scroll Section Nav

Generates a floating vertical dot-nav from h2 headings on the page with smooth-scroll and active-section highlighting.

Module3 parts · by SnipCraft

PHPMarkup

php
<?php
if ( ! function_exists( 'scseed_section_nav_output' ) ) {
    function scseed_section_nav_output() {
        if ( ! is_singular( 'post' ) ) {
            return;
        }
        echo '<nav id="scseed-section-nav" aria-label="' .
             esc_attr__( 'Page sections', 'scseed' ) . '"></nav>';
    }
    add_action( 'wp_footer', 'scseed_section_nav_output' );
}

CSSStyles

css
#scseed-section-nav {
  position: fixed;
  right: 1.25rem;
  top: 50%;
  transform: translateY(-50%);
  z-index: 9000;
  display: flex;
  flex-direction: column;
  gap: 0.5rem;
}
#scseed-section-nav a {
  display: block;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: rgba(0,0,0,0.25);
  text-decoration: none;
  transition: background 0.2s, transform 0.2s;
  position: relative;
}
#scseed-section-nav a:hover,
#scseed-section-nav a.active {
  background: #0073aa;
  transform: scale(1.4);
}
#scseed-section-nav a::after {
  content: attr(data-label);
  position: absolute;
  right: 1.4rem;
  top: 50%;
  transform: translateY(-50%);
  background: rgba(0,0,0,0.75);
  color: #fff;
  font-size: 0.72rem;
  white-space: nowrap;
  padding: 2px 6px;
  border-radius: 3px;
  opacity: 0;
  pointer-events: none;
  transition: opacity 0.2s;
}
#scseed-section-nav a:hover::after {
  opacity: 1;
}
@media (max-width: 900px) {
  #scseed-section-nav { display: none; }
}

JavaScriptScript

js
(function () {
  'use strict';
  var nav = document.getElementById('scseed-section-nav');
  if (!nav) { return; }

  var headings = Array.prototype.slice.call(document.querySelectorAll('.entry-content h2, article h2'));
  if (headings.length < 2) {
    nav.style.display = 'none';
    return;
  }

  headings.forEach(function (h, i) {
    if (!h.id) { h.id = 'scseed-sec-' + i; }
    var a = document.createElement('a');
    a.href = '#' + h.id;
    a.setAttribute('data-label', h.textContent.trim().substring(0, 40));
    a.setAttribute('aria-label', h.textContent.trim());
    nav.appendChild(a);
    a.addEventListener('click', function (e) {
      e.preventDefault();
      h.scrollIntoView({ behavior: 'smooth' });
    });
  });

  var links = Array.prototype.slice.call(nav.querySelectorAll('a'));

  function onScroll() {
    var mid = window.scrollY + window.innerHeight * 0.4;
    var active = null;
    headings.forEach(function (h, i) {
      if (h.getBoundingClientRect().top + window.scrollY <= mid) {
        active = links[i];
      }
    });
    links.forEach(function (l) { l.classList.remove('active'); });
    if (active) { active.classList.add('active'); }
  }

  window.addEventListener('scroll', onScroll, { passive: true });
  onScroll();
})();
#frontend#ux#navigation#accessibility