Page minimap

A miniature of the whole page sits in the corner, with a window showing where you are. Click it to jump.

How it works

The script reads the position, size and colour of every block on the page and redraws them as tiny divs inside a fixed panel, scaled by panel height ÷ page height. A translucent box with the viewport’s proportions moves with scrollY, and clicks on the map convert back to a scroll position.

Full file
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Page minimap</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; }
body { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; -webkit-font-smoothing: antialiased; }

body { background: #f5f5f7; color: #1d1d1f; }
.page { max-width: 720px; margin: 0 auto; padding: 8vh 24px 20vh; }
.blk { border-radius: 10px; margin-bottom: 18px; }
.blk.h { height: 60px; width: 60%; background: #1d1d1f; }
.blk.t { height: 14px; background: #d2d2d7; border-radius: 7px; margin-bottom: 10px; }
.blk.img { height: 260px; }
.map { position: fixed; top: 16px; right: 16px; bottom: 16px; width: 72px; background: #fff; border-radius: 10px; box-shadow: 0 4px 20px rgba(0,0,0,.12); overflow: hidden; cursor: pointer; z-index: 5; }
.map b { position: absolute; display: block; border-radius: 1px; }
.view { position: absolute; left: 0; right: 0; background: rgba(0,113,227,.18); border: 1.5px solid #0071e3; border-radius: 4px; }
@media (max-width: 600px) { .map { width: 44px; } }
</style>
</head>
<body>
<aside class="map" id="map"><div class="view" id="view"></div></aside>
<main class="page" id="page"></main>
<script>
const page = document.getElementById('page');
const colors = ['#ff9f1c', '#2ec4b6', '#e71d36', '#7209b7', '#4361ee'];
for (let s = 0; s < 8; s++) {
  page.insertAdjacentHTML('beforeend', '<div class="blk h"></div>');
  for (let l = 0; l < 4 + (s % 3); l++) page.insertAdjacentHTML('beforeend', '<div class="blk t" style="width:' + (70 + Math.random() * 30) + '%"></div>');
  page.insertAdjacentHTML('beforeend', '<div class="blk img" style="background:' + colors[s % 5] + '"></div>');
}
const map = document.getElementById('map'), view = document.getElementById('view');
let scale = 1;
function build() {
  map.querySelectorAll('b').forEach(b => b.remove());
  const docH = document.documentElement.scrollHeight;
  scale = map.clientHeight / docH;
  const pr = page.getBoundingClientRect();
  const sx = map.clientWidth / pr.width;
  page.querySelectorAll('.blk').forEach(el => {
    const r = el.getBoundingClientRect();
    const b = document.createElement('b');
    b.style.cssText = 'top:' + ((r.top + scrollY) * scale) + 'px;left:' + ((r.left - pr.left) * sx) + 'px;width:' + (r.width * sx) + 'px;height:' + Math.max(1, r.height * scale) + 'px;background:' + getComputedStyle(el).backgroundColor;
    map.insertBefore(b, view);
  });
  view.style.height = (innerHeight * scale) + 'px';
  update();
}
function update() { view.style.top = (scrollY * scale) + 'px'; }
map.addEventListener('click', e => {
  const y = (e.clientY - map.getBoundingClientRect().top) / scale - innerHeight / 2;
  scrollTo({ top: y, behavior: 'smooth' });
});
addEventListener('scroll', update, { passive: true });
addEventListener('resize', build);
build();
</script>
</body>
</html>

More progress effects