Reading progress bar
A thin bar across the top fills as you move through an article.
A miniature of the whole page sits in the corner, with a window showing where you are. Click it to jump.
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.
<!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>