UI Components Easy
Reading Guide
Reading guide line that follows the cursor or scroll position to help users track their reading position across lines of text.
Open in Lab
MCP
vanilla-js css
Targets: JS HTML
Code
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: #0a0a0a;
color: #e4e4e7;
line-height: 1.7;
min-height: 100vh;
}
.container {
display: grid;
grid-template-columns: 260px 1fr;
gap: 2rem;
max-width: 1100px;
margin: 0 auto;
padding: 2rem 1.5rem;
}
/* ── Drawer toggle (hidden on desktop) ── */
.drawer-toggle {
display: none;
}
.drawer-backdrop {
display: none;
}
/* ── Mobile: drawer pattern ── */
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
gap: 0;
padding: 1rem 0.75rem;
}
/* Hide controls as inline block, convert to drawer */
.controls {
position: fixed;
inset-inline-start: 0;
inset-inline-end: 0;
bottom: 0;
top: auto;
z-index: 1000;
border-radius: 16px 16px 0 0;
border: 1px solid rgba(255, 255, 255, 0.1);
border-bottom: none;
background: #111113;
padding: 1.25rem 1.25rem 2rem;
transform: translateY(100%);
transition: transform 0.3s ease;
max-height: 70vh;
overflow-y: auto;
}
.controls.drawer-open {
transform: translateY(0);
}
/* Drawer handle */
.controls::before {
content: "";
display: block;
width: 36px;
height: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 2px;
margin: 0 auto 1rem;
}
.controls__title {
font-size: 0.85rem;
margin-bottom: 1rem;
}
.controls__group {
margin-bottom: 1rem;
}
.controls__modes {
gap: 0.25rem;
}
.mode-btn {
padding: 0.5rem 0.25rem;
font-size: 0.7rem;
}
/* FAB toggle */
.drawer-toggle {
display: flex;
align-items: center;
justify-content: center;
position: fixed;
bottom: 1.25rem;
right: 1.25rem;
z-index: 999;
width: 48px;
height: 48px;
border-radius: 50%;
background: #8b5cf6;
color: #fff;
border: none;
cursor: pointer;
box-shadow: 0 4px 16px rgba(139, 92, 246, 0.4);
transition: transform 0.2s, background 0.2s;
}
.drawer-toggle:hover {
transform: scale(1.05);
}
.drawer-toggle.active {
background: #6d28d9;
transform: rotate(90deg);
}
/* Backdrop */
.drawer-backdrop {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999;
}
.drawer-backdrop.active {
display: block;
}
/* Article adjustments */
.article__title {
font-size: 1.3rem;
margin-bottom: 1rem;
}
.article__subtitle {
font-size: 1.05rem;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
}
.article__text {
font-size: 0.9rem;
margin-bottom: 1rem;
}
}
/* ---- Controls panel ---- */
.controls {
position: sticky;
top: 2rem;
align-self: start;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 12px;
padding: 1.25rem;
}
.controls__title {
font-size: 0.875rem;
font-weight: 600;
color: #a1a1aa;
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 1.25rem;
}
.controls__group {
margin-bottom: 1.25rem;
}
.controls__group:last-child {
margin-bottom: 0;
}
.controls__label {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.875rem;
color: #e4e4e7;
cursor: pointer;
user-select: none;
}
.controls__checkbox {
display: none;
}
.controls__check-icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
border: 2px solid rgba(255, 255, 255, 0.2);
border-radius: 5px;
transition: all 0.2s ease;
flex-shrink: 0;
}
.controls__checkbox:checked + .controls__check-icon {
background: #8b5cf6;
border-color: #8b5cf6;
}
.controls__checkbox:checked + .controls__check-icon::after {
content: "";
width: 5px;
height: 9px;
border: solid #fff;
border-width: 0 2px 2px 0;
transform: rotate(45deg) translateY(-1px);
}
.controls__label-text {
display: block;
font-size: 0.75rem;
font-weight: 500;
color: #71717a;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 0.5rem;
}
/* Modes */
.controls__modes {
display: flex;
gap: 0.375rem;
}
.mode-btn {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
padding: 0.5rem 0.25rem;
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 8px;
background: transparent;
color: #71717a;
font-size: 0.6875rem;
cursor: pointer;
transition: all 0.2s ease;
}
.mode-btn:hover {
background: rgba(255, 255, 255, 0.04);
color: #a1a1aa;
}
.mode-btn.active {
background: rgba(139, 92, 246, 0.1);
border-color: rgba(139, 92, 246, 0.3);
color: #c4b5fd;
}
/* Slider */
.controls__slider-row {
display: flex;
align-items: center;
gap: 0.75rem;
}
.controls__range {
flex: 1;
appearance: none;
height: 4px;
background: rgba(255, 255, 255, 0.1);
border-radius: 2px;
outline: none;
}
.controls__range::-webkit-slider-thumb {
appearance: none;
width: 16px;
height: 16px;
background: #8b5cf6;
border-radius: 50%;
cursor: pointer;
}
.controls__value {
font-size: 0.75rem;
color: #71717a;
min-width: 36px;
text-align: right;
font-variant-numeric: tabular-nums;
}
/* Color swatches */
.controls__color-row {
display: flex;
gap: 0.5rem;
}
.color-swatch {
width: 24px;
height: 24px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.2s ease;
}
.color-swatch:hover {
transform: scale(1.15);
}
.color-swatch.active {
border-color: #fff;
box-shadow: 0 0 0 2px rgba(255, 255, 255, 0.2);
}
/* ---- Article ---- */
.article__title {
font-size: 1.75rem;
font-weight: 700;
color: #f4f4f5;
margin-bottom: 1.5rem;
letter-spacing: -0.02em;
line-height: 1.3;
}
.article__subtitle {
font-size: 1.25rem;
font-weight: 600;
color: #e4e4e7;
margin-top: 2rem;
margin-bottom: 1rem;
}
.article__text {
font-size: 1rem;
color: #a1a1aa;
margin-bottom: 1.25rem;
max-width: 65ch;
}
/* ---- Guide overlays ---- */
.guide {
position: fixed;
left: 0;
width: 100%;
pointer-events: none;
z-index: 9999;
opacity: 0;
transition: opacity 0.2s ease;
}
.guide.visible {
opacity: 1;
}
.guide--line {
height: 2px;
background: var(--guide-color, #8b5cf6);
box-shadow: 0 0 8px var(--guide-color, #8b5cf6);
}
.guide--band {
height: var(--guide-height, 40px);
background: var(--guide-color, #8b5cf6);
opacity: 0;
border-radius: 2px;
}
.guide--band.visible {
opacity: 0.12;
}
.guide-overlay {
position: fixed;
left: 0;
width: 100%;
background: rgba(0, 0, 0, 0.65);
pointer-events: none;
z-index: 9999;
opacity: 0;
transition: opacity 0.2s ease;
}
.guide-overlay.visible {
opacity: 1;
}
.guide-overlay--top {
top: 0;
}
.guide-overlay--bottom {
bottom: 0;
}(() => {
const guideToggle = document.getElementById("guide-toggle");
const heightSlider = document.getElementById("guide-height");
const heightValue = document.getElementById("guide-height-value");
const modeBtns = document.querySelectorAll(".mode-btn");
const colorSwatches = document.querySelectorAll(".color-swatch");
const guideLine = document.getElementById("guide-line");
const guideBand = document.getElementById("guide-band");
const overlayTop = document.getElementById("guide-overlay-top");
const overlayBottom = document.getElementById("guide-overlay-bottom");
let enabled = false;
let mode = "line";
let guideHeight = 40;
let guideColor = "#8b5cf6";
function hideAllGuides() {
guideLine.classList.remove("visible");
guideBand.classList.remove("visible");
overlayTop.classList.remove("visible");
overlayBottom.classList.remove("visible");
}
function positionGuide(y) {
if (!enabled) return;
hideAllGuides();
if (mode === "line") {
guideLine.style.top = y + "px";
guideLine.style.setProperty("--guide-color", guideColor);
guideLine.classList.add("visible");
} else if (mode === "band") {
const top = y - guideHeight / 2;
guideBand.style.top = top + "px";
guideBand.style.height = guideHeight + "px";
guideBand.style.setProperty("--guide-color", guideColor);
guideBand.classList.add("visible");
} else if (mode === "spotlight") {
const halfH = guideHeight / 2;
overlayTop.style.height = Math.max(0, y - halfH) + "px";
overlayBottom.style.top = y + halfH + "px";
overlayBottom.style.height = window.innerHeight - y - halfH + "px";
overlayTop.classList.add("visible");
overlayBottom.classList.add("visible");
}
}
function setMode(newMode) {
mode = newMode;
modeBtns.forEach((btn) => {
btn.classList.toggle("active", btn.dataset.mode === newMode);
});
hideAllGuides();
}
function setColor(color) {
guideColor = color;
colorSwatches.forEach((s) => {
s.classList.toggle("active", s.dataset.color === color);
});
}
// Event listeners
guideToggle.addEventListener("change", () => {
enabled = guideToggle.checked;
if (!enabled) hideAllGuides();
});
modeBtns.forEach((btn) => {
btn.addEventListener("click", () => setMode(btn.dataset.mode));
});
heightSlider.addEventListener("input", () => {
guideHeight = parseInt(heightSlider.value, 10);
heightValue.textContent = guideHeight + "px";
});
colorSwatches.forEach((swatch) => {
swatch.addEventListener("click", () => setColor(swatch.dataset.color));
});
document.addEventListener("mousemove", (e) => {
requestAnimationFrame(() => positionGuide(e.clientY));
});
// Touch support
document.addEventListener(
"touchmove",
(e) => {
const touch = e.touches[0];
requestAnimationFrame(() => positionGuide(touch.clientY));
},
{ passive: true }
);
// ── Mobile drawer ──
const drawerToggle = document.getElementById("drawer-toggle");
const drawerBackdrop = document.getElementById("drawer-backdrop");
const controls = document.getElementById("controls");
function openDrawer() {
controls.classList.add("drawer-open");
drawerBackdrop.classList.add("active");
drawerToggle.classList.add("active");
}
function closeDrawer() {
controls.classList.remove("drawer-open");
drawerBackdrop.classList.remove("active");
drawerToggle.classList.remove("active");
}
if (drawerToggle) {
drawerToggle.addEventListener("click", () => {
const isOpen = controls.classList.contains("drawer-open");
if (isOpen) closeDrawer();
else openDrawer();
});
}
if (drawerBackdrop) {
drawerBackdrop.addEventListener("click", closeDrawer);
}
window.addEventListener("resize", () => {
if (window.innerWidth > 768) closeDrawer();
});
})();<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Reading Guide</title>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="container">
<!-- Controls -->
<aside class="controls" id="controls">
<h2 class="controls__title">Reading Guide</h2>
<div class="controls__group">
<label class="controls__label">
<input type="checkbox" id="guide-toggle" class="controls__checkbox" />
<span class="controls__check-icon"></span>
Enable Guide
</label>
</div>
<div class="controls__group">
<span class="controls__label-text">Mode</span>
<div class="controls__modes">
<button class="mode-btn active" data-mode="line" title="Thin line">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><rect x="1" y="8" width="16" height="2" rx="1" fill="currentColor"/></svg>
Line
</button>
<button class="mode-btn" data-mode="band" title="Highlight band">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><rect x="1" y="5" width="16" height="8" rx="2" fill="currentColor" opacity="0.4"/></svg>
Band
</button>
<button class="mode-btn" data-mode="spotlight" title="Spotlight">
<svg width="18" height="18" viewBox="0 0 18 18" fill="none"><rect x="0" y="0" width="18" height="6" fill="currentColor" opacity="0.3"/><rect x="0" y="12" width="18" height="6" fill="currentColor" opacity="0.3"/></svg>
Spotlight
</button>
</div>
</div>
<div class="controls__group">
<label class="controls__label-text" for="guide-height">Guide Height</label>
<div class="controls__slider-row">
<input type="range" id="guide-height" class="controls__range" min="2" max="120" value="40" />
<span class="controls__value" id="guide-height-value">40px</span>
</div>
</div>
<div class="controls__group">
<label class="controls__label-text" for="guide-color">Color</label>
<div class="controls__color-row">
<button class="color-swatch active" data-color="#8b5cf6" style="background:#8b5cf6" title="Purple"></button>
<button class="color-swatch" data-color="#3b82f6" style="background:#3b82f6" title="Blue"></button>
<button class="color-swatch" data-color="#10b981" style="background:#10b981" title="Green"></button>
<button class="color-swatch" data-color="#f59e0b" style="background:#f59e0b" title="Amber"></button>
<button class="color-swatch" data-color="#ef4444" style="background:#ef4444" title="Red"></button>
</div>
</div>
</aside>
<!-- Article content -->
<main class="content" id="content">
<article class="article">
<h1 class="article__title">The Science of Comfortable Reading</h1>
<p class="article__text">
Reading on screens presents unique challenges that differ significantly from reading on paper. One of the most common difficulties is losing track of which line you are on, especially in long paragraphs or dense text. This phenomenon, sometimes called "line skipping," affects a wide range of readers, including those with dyslexia, ADHD, low vision, or simply anyone reading lengthy content on a wide monitor.
</p>
<p class="article__text">
A reading guide -- sometimes called a reading ruler or typoscope -- addresses this problem by providing a visual anchor that highlights or isolates the current line of text. Physical reading rulers have been used in education for decades, and their digital equivalents bring the same benefits to screen-based reading. The guide follows your cursor position, creating a clear focal point that reduces eye strain and prevents line-skipping.
</p>
<h2 class="article__subtitle">Why Line Tracking Matters</h2>
<p class="article__text">
Research in cognitive psychology has shown that our eyes do not move smoothly across text. Instead, they make rapid jumps called saccades, pausing briefly at fixation points to process clusters of characters. When the eye reaches the end of a line and must return to the beginning of the next, it makes a particularly long saccade called a return sweep. It is during these return sweeps that readers most often lose their place.
</p>
<p class="article__text">
The longer the line of text, the longer the return sweep, and the higher the probability of landing on the wrong line. This is one reason why style guides recommend limiting line length to around 60-75 characters. But even with optimal line length, fatigue, screen glare, and cognitive load can all contribute to line-skipping. A reading guide provides an extra layer of support by giving the eye a persistent visual reference point.
</p>
<h2 class="article__subtitle">Modes of Guidance</h2>
<p class="article__text">
Different readers prefer different styles of guidance. The three most common approaches are the ruler line, the highlight band, and the spotlight. The ruler line places a thin, colored line at the cursor's vertical position, mimicking the edge of a physical ruler. It is minimal and unobtrusive, suitable for readers who need only a gentle reference.
</p>
<p class="article__text">
The highlight band extends a semi-transparent colored overlay across the full width of the text, centered on the cursor's vertical position. It typically spans one to three lines of text and draws attention to the current reading area while keeping surrounding text visible. Many readers find this the most comfortable option for extended reading sessions.
</p>
<p class="article__text">
The spotlight or dimming mode takes the opposite approach: rather than highlighting the current area, it dims everything else. A transparent strip remains at cursor height while the text above and below is covered by a semi-opaque overlay. This creates a strong focus effect that can be particularly helpful for readers with attention difficulties, as it reduces the visual noise from surrounding content.
</p>
<h2 class="article__subtitle">Customization and Comfort</h2>
<p class="article__text">
No single setting works for every reader, which is why adjustability is essential. The guide height should be configurable to match different font sizes and line heights. Color options allow readers to choose a hue that provides sufficient contrast without causing discomfort. Some readers find warm colors like amber less fatiguing, while others prefer cool blues or greens. The ability to toggle the guide on and off quickly is also important, as readers may want it only for dense paragraphs or unfamiliar content.
</p>
<p class="article__text">
Persistence of preferences is a thoughtful touch that demonstrates respect for the user's time and autonomy. When a reader has found their ideal configuration, saving those settings to localStorage ensures they do not have to repeat the setup process on every visit. This kind of attention to detail distinguishes truly inclusive design from checkbox compliance.
</p>
</article>
</main>
</div>
<!-- Mobile drawer toggle -->
<button class="drawer-toggle" id="drawer-toggle" aria-label="Open reading guide settings">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
</button>
<!-- Mobile backdrop -->
<div class="drawer-backdrop" id="drawer-backdrop"></div>
<!-- Guide elements -->
<div class="guide guide--line" id="guide-line" aria-hidden="true"></div>
<div class="guide guide--band" id="guide-band" aria-hidden="true"></div>
<div class="guide-overlay guide-overlay--top" id="guide-overlay-top" aria-hidden="true"></div>
<div class="guide-overlay guide-overlay--bottom" id="guide-overlay-bottom" aria-hidden="true"></div>
<script src="script.js"></script>
</body>
</html>A cursor-following reading guide that helps users track their position in long-form text. Offers three modes — a thin ruler line, a highlighted band, and a spotlight/dimming effect — all togglable and adjustable in height.