npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@lp-labs/react-reading-progress

v0.1.0

Published

Scoped React reading progress and optional position restoration.

Readme

@lp-labs/react-reading-progress

Element-scoped reading progress for React 18.2 and 19. Track a scroll container, report sections, and optionally restore a saved pixel position. No window scroll tracking or module-global reading state. React is the only peer and there are no runtime dependencies.

Install and use

npm install @lp-labs/react-reading-progress react
import { useMemo, useRef } from "react";
import { ReadingProgress } from "@lp-labs/react-reading-progress";
import "@lp-labs/react-reading-progress/style.css";

export function Reader() {
  const containerRef = useRef<HTMLDivElement>(null);
  const introduction = useRef<HTMLElement>(null);
  const details = useRef<HTMLElement>(null);
  const sections = useMemo(() => [
    { id: "introduction", ref: introduction },
    { id: "details", ref: details },
  ], []);
  return (
    <>
      <ReadingProgress
        containerRef={containerRef}
        sections={sections}
        storageKey="reader:article-one:v1"
        restore
        onChange={({ activeSection }) => console.log(activeSection)}
        onStorageError={(error) => console.warn("Position not saved", error)}
      />
      <div ref={containerRef} style={{ height: 300, overflowY: "auto" }}>
        <section ref={introduction} style={{ minHeight: 500 }}>Introduction</section>
        <section ref={details} style={{ minHeight: 500 }}>Details</section>
      </div>
    </>
  );
}

Exports and types

ReadingSection is { id: string; ref: RefObject<HTMLElement | null> }. Pass sections in document order with stable IDs. Sections outside the container are ignored. ReadingProgressState is { progress: number; activeSection: string | null }. Progress is a fraction from 0 to 1, not a percentage.

useReadingProgress(options: ReadingProgressOptions): ReadingProgressState and ReadingProgress(props: ReadingProgressProps) share these options:

| Option | Type | Default / behavior | | --- | --- | --- | | containerRef | RefObject<HTMLElement \| null> | Required; the scrolling element, not merely its content. | | sections | readonly ReadingSection[] | Omitted; active section is null. | | storageKey | string | Omitted/empty: no storage access. Use a document/version-specific key. | | storage | Pick<Storage, "getItem" \| "setItem"> | Omitted: container window's sessionStorage, accessed only when a key is set. | | restore | boolean | false; whether to read a saved position on effect setup. Saving still occurs with a key when restore is false. | | onChange | (state: ReadingProgressState) => void | Optional; initial measurement, then changed progress or section. | | onStorageError | (error: unknown) => void | Optional; storage access/read/write exceptions, otherwise silently ignored. |

ReadingProgressProps adds label?: string (default "Reading progress"), className?: string (default "", appended to lp-reading-progress), and style?: CSSProperties (no default). The component renders a native <progress> with max={1}, current value, label, and percentage fallback text. It does not display section names; use the hook for a custom UI.

The hook initially returns { progress: 0, activeSection: null } before the mount measurement. An active section is the last supplied in-container section whose top has crossed the container's inner top (one-pixel tolerance). No crossed section means null. onChange runs after measurements, including first setup. Keep options/ref objects/section arrays stable to avoid effect resets and repeated restore. Callback references may change without reattaching listeners.

calculateReadingProgress(scrollTop: number, scrollHeight: number, clientHeight: number): number computes scrollTop / max(0, scrollHeight - clientHeight) clamped to 0-1. Content that does not overflow is complete (1). Nonfinite inputs return 0. It is pure, does not access the DOM, and does not throw for numeric edge cases.

Custom presentation

import { useRef } from "react";
import {
  useReadingProgress,
  calculateReadingProgress,
} from "@lp-labs/react-reading-progress";

export function CompactReader() {
  const containerRef = useRef<HTMLDivElement>(null);
  const { progress, activeSection } = useReadingProgress({ containerRef });
  return (
    <>
      <output>{Math.round(progress * 100)}% {activeSection ?? ""}</output>
      <div ref={containerRef} style={{ height: 250, overflow: "auto" }}>
        <p style={{ minHeight: 900 }}>Long content.</p>
      </div>
      <small>Example metric: {calculateReadingProgress(400, 1000, 200)}</small>
    </>
  );
}

Restoration, lifecycle, and browser limits

Saved values are plain scrollTop pixel strings. Missing/nonfinite values are ignored; restored values are clamped to the current scrollable range. Restoration runs once per effect setup, not after delayed content arrives. Load content before mounting or restore at the application level if layout changes later. Keys have no implicit text versioning or cross-tab synchronization. Storage is caller-owned; no entries are removed.

The hook listens to the element's scroll event and its owning window's resize event, observes container/direct-child size changes with ResizeObserver when available, and observes descendant child/text changes with MutationObserver. Size observation is refreshed after mutations. It measures synchronously on scroll, with no animation loop or throttling. Large section lists may need an application-level alternative. Transform-only layout shifts and attribute-only changes are not comprehensively detected. Without ResizeObserver, scroll, resize, and text/child changes still refresh. On cleanup it saves the position, removes listeners, and disconnects both observers.

Use mounted stable refs; ref target replacement alone does not trigger an effect. Vertical scrolling only; no document/window scrolling, horizontal progress, virtualized reading model, or proof of actual reading. Imports and SSR need no browser globals; browser measurement starts in effects. Exceptions from caller callbacks propagate; storage exceptions are handled as above. Development Strict Mode may measure/save more than once.

CSS and accessibility

Import CSS explicitly and customize --reading-accent or .lp-reading-progress. Native progress semantics avoid live announcements on every scroll. Give multiple indicators distinct labels. Short content being complete is intentional. The hook does not move focus, intercept keys, or change scroll position unless restoration is explicitly enabled.

License

MIT.

Support

If this library is useful to you, consider supporting its maintenance with a donation via PayPal.

Author

Created by Libin Prasanth. Connect on LinkedIn for updates and discussion.