Sticky Nav Block
A Gutenberg block that displays a set of in-page links from matching tags. Includes sticky and inline options.
by Highbrow Interactive · github.com/hughc/wp-sticky-nav-block
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/hughc/wp-sticky-nav-block/archive/refs/heads/main.zipA Gutenberg block that renders a navigational table of contents with smooth scroll, scrollspy, and responsive positioning.
Block Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
tagsList |
string | "h2" |
Comma-separated heading tags to include (e.g. "h2,h3,h4") |
excludeClass |
string | "" |
CSS class — headings with this class are skipped |
menuTag |
string | "ul" |
Menu list tag: "ul" or "ol" |
headerText |
string | "Table of Contents" |
Heading text shown above the menu |
collapsible |
boolean | false |
On mobile (<768px), menu collapses behind a toggle button |
scrollToOffset |
integer | 0 |
Extra px subtracted from scroll-to-target position |
topOffset |
integer | 80 |
Sets --snb-top — distance from top of viewport when pinned |
panelWidth |
integer | 280 |
Sets --snb-width — panel width when fully expanded |
showAfterScroll |
integer | 0 |
Block stays hidden until user scrolls this many px, then fades in |
desktopBreakpoint |
integer | 768 |
Viewport width (px) at which the block becomes fixed-position |
collapseBreakpoint |
integer | 1200 |
Viewport width (px) at which the collapse pill stops appearing |
Behaviour
- Mobile (<768px): inline flow, optional collapsible toggle.
- Tablet/narrow desktop (768–1199px): fixed right-panel, collapses to a compact icon pill that toggles open/closed. Gets around the issue
- Desktop (≥1200px): fixed right-panel, full menu always visible.
CSS Custom Properties
All values below are defaults set by the plugin. Override any of them in your theme's style to restyle or change behaviour.
s
| Variable | Default | Controls |
| ------------------------ | ------------------------------ | ------------------------------------------------ |
| --snb-top | 80px | Distance from top of viewport when pinned (set by topOffset attribute) |
| --snb-right | 20px | Distance from right edge when pinned |
| --snb-width | 280px | Panel width when fully expanded (set by panelWidth attribute) |
| --snb-max-height | calc(100vh - var(--snb-top)) | Max panel height (auto-derived) |
| --snb-z-index | 999 | Stacking order |
| --snb-transition-speed | 0.2s | Fade transition speed |
| --snb-bg | #fff | Panel background colour |
| --snb-border | #e0e0e0 | Panel border colour |
| --snb-active-color | #0073aa | Active link colour & hover accent |
| --snb-collapse-width | 1200px | Viewport width below which collapse pill appears |
| --snb-collapsed-width | 44px | Reference width for collapsed pill |
| --snb-icon-color | currentColor | SVG icon colour |
Architecture
PHP (server-side rendering)
The block is rendered dynamically via render_callback — no save() function is used. The front-end flow has three stages:
-
snb_render_block()— outputs the block's wrapper HTML (header, title, toggle buttons, empty.snb-block__bodyplaceholder) and stores block attributes in the global$snb_config. Also hooksthe_contentatPHP_INT_MAXpriority so it runs after all other content filters. -
snb_the_content_filter()— runs onthe_content. Uses$snb_configto know which headings to look for. Callssnb_process_content()to scan the rendered post HTML, then injects the generated menu into the block's.snb-block__bodyplaceholder via regex replacement. Clears$snb_configafter one run so it only fires once per page. -
snb_process_content()— walks the full post HTML usingWP_HTML_Tag_Processor. For every heading matching the configured tags (and not excluded by class), it:- Extracts the heading text and level (h2, h3, etc.)
- Injects an
idattribute if one doesn't already exist (generated from the heading text viasanitize_title()) - Returns the modified HTML plus a structured array of headings for menu building
-
snb_build_menu()/snb_render_heading_items()— recursively builds a nested<ul>/<ol>from the heading array. Nesting is determined by heading level changes (e.g. h2 → h3 opens a nested list, h3 → h2 closes it).
Frontend JavaScript (src/frontend.js)
A single IIFE runs on page load. It finds every [data-snb] element and sets up:
-
Smooth scroll — click handlers on
[data-snb-link]elements scroll to the target heading, accounting forscrollToOffset. -
updateActiveLink()— scroll listener (passive). Does two things:- Visibility fade: if
showAfterScroll > 0and the block is pinned, sets--snb-visibilityandpointer-eventsto show/hide the block based on scroll position. - Scrollspy: walks headings to find the deepest heading whose top is above the current scroll position, then marks its link with
snb-block__link--active.
- Visibility fade: if
-
Mobile collapsible toggle — if the block has
snb-block--collapsible, a toggle button shows/hides.snb-block__bodyon mobile (<768px) viaaria-expanded. -
Collapse icon toggle — the
.snb-block__collapse-btnswaps between collapsed/expanded state on tablet/narrow desktop. Reads the current state from the class list (not a JS variable) so it stays in sync withmaybePin()on load and resize. -
maybePin()— breakpoint-driven layout engine called on load and resize. Three states:- Mobile (
vw < desktopBreakpoint): removes fixed positioning, restores inline flow. - Narrow desktop (
desktopBreakpoint <= vw < collapseBreakpoint): pins the block fixed, starts collapsed to the icon pill. - Full desktop (
vw >= collapseBreakpoint): pins the block fixed, always expanded.
Uses a placeholder
<div>to preserve document flow height when the block is removed from normal flow for fixed positioning. - Mobile (
-
updateCollapseBtnVisibility(vw)— shows or hides the collapse button based on the current viewport width vs the per-blockdesktopBreakpointandcollapseBreakpointvalues (read fromdata-desktop-bp/data-collapse-bpattributes). No CSS media queries are used for this — JS is the single source of truth.
Updates
If you utilise the Git Updater plugin, you can get patches delivered when new versions are published to Github.
BEMIT Styling Skeleton
Copy the rules below into your theme's style.scss to override plugin styles.
Selectors are listed with zero declarations — add properties as needed.
/* ===== Sticky Nav Block overrides ===== */
// Root CSS variables (override plugin defaults)
:root {
--snb-top: 80px;
--snb-right: 20px;
--snb-width: 280px;
--snb-max-height: calc(100vh - var(--snb-top));
--snb-z-index: 999;
--snb-transition-speed: 0.2s;
--snb-bg: #fff;
--snb-border: #e0e0e0;
--snb-active-color: #0073aa;
--snb-collapse-width: 1200px;
--snb-collapsed-width: 44px;
--snb-icon-color: currentColor;
}
.snb-block {
// ── Modifiers ──
// Pinned (fixed) state
&--pinned {
}
// Collapsed pill state (768–1199px)
&--pinned&--collapsed {
}
&--pinned&--collapsed &__header {
}
// Collapsed state — hides body, title; swaps icons
&--collapsed {
}
&--collapsed &__body {
}
&--collapsed &__title {
}
&--collapsed &__icon--open {
}
&--collapsed &__icon--close {
}
// ── Elements ──
// Header row (title + buttons)
&__header {
}
// Title text
&__title {
}
// Collapse toggle button (tablet/narrow desktop)
&__collapse-btn {
}
// SVG icons inside collapse button
&__icon {
}
&__icon--open {
} // hamburger — visible when collapsed (click to expand)
&__icon--close {
} // X — visible when expanded (click to collapse)
// Mobile toggle button (<768px)
&__toggle {
}
&__toggle-icon {
}
// Menu container
&__body {
}
// Menu list
&__list {
}
&__list--nested {
}
// Individual menu item
&__item {
}
// Individual menu link
&__link {
}
&__link:hover {
}
&__link--active {
}
}