Reading Progress

A thin bar across the top fills as you read down the article, so readers always know how much is left.

  • JS
  • Layout
  • Scroll
  • Added
0% read

Designing calm interfaces

AI prompt

Create a reading progress bar for an article in React. Listen to the scroll event of the scrolling element (window for a full page) with a passive listener, compute progress as scrollTop / (scrollHeight - clientHeight), and write it into a CSS custom property --p on the container instead of React state, so scrolling never re-renders. A sticky bar at the top uses transform: scaleX(var(--p)) with transform-origin left and an accent background; next to it, a small mono label shows the rounded percentage. Do not transition the bar, so it tracks the scroll exactly. On browsers that support animation-timeline: scroll(), this can be done in pure CSS instead. Under prefers-reduced-motion it stays as is, since it only moves when the user scrolls. The article title reads "Designing calm interfaces".

Source

Demo.tsx
'use client'

import { useRef } from 'react'
import s from './demo.module.css'

export default function ScrollProgress({ labels }: { labels: Record<string, string> }) {
  const pct = useRef<HTMLSpanElement>(null)

  const onScroll = (e: React.UIEvent<HTMLDivElement>) => {
    const el = e.currentTarget
    const p = el.scrollTop / (el.scrollHeight - el.clientHeight || 1)
    el.style.setProperty('--p', String(p))
    if (pct.current) pct.current.textContent = `${Math.round(p * 100)}% ${labels.read}`
  }

  return (
    <div className={s.frame} tabIndex={0} onScroll={onScroll}>
      <div className={s.top}>
        <span className={s.bar} />
        <span ref={pct} className={s.pct}>
          0% {labels.read}
        </span>
      </div>
      <article className={s.article}>
        <h3 className={s.title}>{labels.title}</h3>
        {Array.from({ length: 14 }, (_, i) => (
          <span key={i} className={s.line} style={{ width: `${[100, 94, 98, 70][i % 4]}%` }} />
        ))}
      </article>
    </div>
  )
}
demo.module.css
.frame {
  position: absolute;
  inset: 0;
  overflow-y: auto;
  overscroll-behavior: contain;
  scrollbar-width: thin;
}

.frame:focus-visible { outline: 2px solid var(--color-focus); outline-offset: -4px; }

.hint {
  display: block;
  font: 500 var(--text-xs) / 1 var(--font-mono);
  letter-spacing: 0.06em;
  text-transform: uppercase;
  text-align: center;
  color: var(--stage-muted);
}

.frame { --p: 0; }

.top {
  position: sticky;
  top: 0;
  z-index: 1;
  display: flex;
  justify-content: flex-end;
  padding: 10px 16px 8px;
  background: var(--stage-bg);
}

.bar {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  height: 3px;
  background: var(--stage-accent);
  transform: scaleX(var(--p));
  transform-origin: left;
}

.pct {
  font: 500 var(--text-xs) / 1 var(--font-mono);
  font-variant-numeric: tabular-nums;
  color: var(--stage-muted);
}

.article {
  display: grid;
  gap: 12px;
  max-width: 300px;
  margin: 0 auto;
  padding: 8px 16px 40px;
}

.title {
  margin: 0 0 6px;
  font: 600 var(--text-xl) / 1.2 var(--font-display);
  color: var(--stage-ink);
}

.line {
  display: block;
  height: 8px;
  border-radius: 4px;
  background: var(--stage-surface);
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.