Skip to content

Instantly share code, notes, and snippets.

@gibson042
Last active May 27, 2026 15:01
Show Gist options
  • Select an option

  • Save gibson042/73dfb17bfa7d35a9fb2e4c396365c321 to your computer and use it in GitHub Desktop.

Select an option

Save gibson042/73dfb17bfa7d35a9fb2e4c396365c321 to your computer and use it in GitHub Desktop.
Anchors Up user script
// ==UserScript==
// @name Anchors Up
// @namespace https://github.com/gibson042
// @description Navigates to in-page anchors upon {Command,Ctrl}+click or (with Shift) scrolls to arbitrary elements.
// @source https://gist.github.com/gibson042/73dfb17bfa7d35a9fb2e4c396365c321
// @updateURL https://gist.github.com/gibson042/73dfb17bfa7d35a9fb2e4c396365c321/raw/anchors-up.user.js
// @downloadURL https://gist.github.com/gibson042/73dfb17bfa7d35a9fb2e4c396365c321/raw/anchors-up.user.js
// @version 0.6.0
// @date 2026-05-27
// @author Richard Gibson <@gmail.com>
// @include *
// ==/UserScript==
//
// **COPYRIGHT NOTICE**
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related and neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
// For the CC0 Public Domain Dedication, see
// <https://creativecommons.org/publicdomain/zero/1.0/>.
//
// **END COPYRIGHT NOTICE**
//
//
// Changelog:
// 0.6.0 (2026-05-27)
// * New: Scan down from heading elements.
// 0.5.0 (2023-09-08)
// * New: CC0 Public Domain Dedication.
// 0.4.0 (2022-11-30)
// * New: Scroll to arbitrary locations with {Command,Ctrl}+Shift+click.
// 0.3.3 (2022-03-21)
// * Improved: Better OS and user script manager compatibility.
// 0.3.1 (2022-02-02)
// * Fixed: Require that Ctrl be the *only* active modifier key.
// 0.3.0 (2019-06-03)
// * Improved: Descend into single-child wrapping elements.
// 0.2.0 (2018-06-26)
// * Improved: Text node children stand in for click targets.
// 0.1.0 (2018-03-21)
// original release
(function() {
"use strict";
const HEADING_PATT = /^(?:hgroup|h[0-9]+)$/i;
// When traversing the DOM seeking a parent anchor, allow detours to a bounded number of previous siblings.
const PREV_LIMIT = 1;
document.addEventListener("click", function( evt ) {
// If Command/Control is not pressed or any other modifier key other than Shift is, abort.
if ( !((evt.ctrlKey ^ evt.metaKey) && !evt.altKey) ) {
return;
}
let anchor,
el = evt.target,
prev = 0,
seen = new Set();
// If Shift is pressed, scroll directly to the target.
if (evt.shiftKey) {
el.scrollIntoView();
const selection = window.getSelection();
selection.collapseToEnd(selection.focusNode, selection.focusOffset);
return;
}
// If the target has child text nodes, let the first one stand in for it.
if ( el.childNodes.length > el.childElementCount ) {
el = Array.prototype.find.call(
el.childNodes,
function(node){ return node.nodeType === document.TEXT_NODE }
);
}
while ( el ) {
seen.add(el);
// Upon finding a link ancestor, quit to allow native navigation.
if ( prev === 0 && el.nodeName.toLowerCase() === "a" && el.href ) {
return;
}
// Check for an anchor, allowing descent into single-child parents
// such as wrapping <p>s from Markdown rendering.
anchor = el.id || el.name;
if ( !anchor && el.childElementCount === 1 ) {
anchor = el.firstElementChild.id || el.firstElementChild.name;
}
// If the element is a heading, its anchor may be nested
// (first seen at https://nodejs.org/api/).
if ( !anchor && HEADING_PATT.test(el.nodeName) ) {
const elCandidates = el.querySelectorAll("[id],[name]");
const strCandidates = Array.prototype.flatMap.call(elCandidates, function(c){
const id = c.id, name = c.name;
return [id, name].filter(Boolean);
});
// Take the shortest anchor, resolving ties by preferring a later position.
strCandidates.reverse().sort(function(a, b){ return a.length - b.length });
anchor = strCandidates.at(0);
}
// Upon finding an anchor, navigate to it and quit.
if ( anchor ) {
location.assign("#" + anchor);
return;
}
// If the element has a not-yet-visited heading child, try that next.
const elHeadingChild = el.children && Array.prototype.find.call(
el.children,
function(c){ return HEADING_PATT.test(c.nodeName); }
);
if ( elHeadingChild && !seen.has(elHeadingChild) ) {
el = elHeadingChild;
continue;
}
// Otherwise, traverse back or up.
el = (++prev <= PREV_LIMIT && el.previousElementSibling) || (prev = 0, el.parentNode);
while ( seen.has(el) ) el = el.parentNode;
}
});
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment