Skip to content
txbrendan/ui

Scroll Progress

beta

A reading-progress pill with a ring, the current section, and an expandable menu for jumping through a page.

Preview

01 / Overview

Make room to read

A long guide is easier to explore when you know where you are. Start at the beginning, follow the sections in order, or jump straight to the part you need.

Scroll inside this panel. The ring fills as you move, and the pill follows the section in view. Open it to see all four sections.

02 / Reading rhythm

One idea at a time

Give each section a clear heading and enough space to develop its idea. Short paragraphs give readers a natural place to pause before moving on.

Good headings help people scan a page and find their way back. Use the same names in the document and its navigation.

03 / Finding your place

A quiet way to navigate

Navigation can stay close without taking over the page. This small control holds your current position until you want to explore somewhere else.

Choose a section from the menu to move there. You can also open the menu with the keyboard and use the arrow keys to pick a destination.

04 / Next steps

Leave with a next step

A useful ending gives readers a place to go. Summarize what they can do with what they have learned, then make the next action easy to find.

You have reached the last section. Keep scrolling to fill the ring, or open the menu to return to the overview.

Disabled

No sections

Install

  1. 1. Add the registry to components.json, once per app. Without it, the shadcn CLI looks up @txbrendan in a public list that txbrendan/ui does not control.

    npx shadcn@latest registry add "@txbrendan=https://txbrendan.dev/r/{name}.json"
  2. 2. Add the theme, once per app. It replaces your color tokens with ones checked for contrast.

    npx shadcn@latest add @txbrendan/theme
  3. 3. Add the component.

    npx shadcn@latest add @txbrendan/scroll-progress

Use it for

  • Keep a reader's position visible in an article or long guide.
  • Pass sections in reading order, with ids matching mounted elements.
  • Track the window by default. Pass containerRef or containerId for an independent scroll surface.
  • Place the indicator outside the scrolling content. Use className='absolute' inside a positioned preview wrapper.

Avoid it for

  • Task completion or uploads. Use Progress for work with a known completion value.
  • Short content with no useful section jumps. Keep the page simple.
  • Replacing visible headings. The menu supplements the document's structure.

Props

  • sections

    Unique section ids and labels in reading order. An empty list shows a disabled pill.

    readonly { id: string; label: string }[] · default []

  • containerRef

    A mounted scrolling element. Takes precedence over containerId. Omit both to track the window.

    RefObject<HTMLElement | null>

  • containerId

    The id of a mounted scrolling element, for composition without a client ref. A missing container does not track the window.

    string

  • offset

    Pixels below the scroll surface's top edge at which a section becomes current. Also leaves space above a jump target.

    number · default 24

  • disabled

    Stops the menu from opening. Reading progress continues to update.

    boolean · default false

  • aria-label

    Names the menu. The trigger also includes the current section and the percentage read.

    string · default "Sections"

  • className

    Classes for the root, merged last. Fixed at bottom center by default. Override placement for an embedded scroll surface.

    string

Accessibility

  • The trigger exposes its expanded state, current section, and percentage read. Progress is not a live announcement on every scroll.
  • Enter, Space, or Arrow Down opens the menu. Arrow keys, Home, End, and typing move between sections.
  • Escape closes the menu and restores trigger focus. Clicking outside also closes it.
  • The active section carries aria-current='location' and a filled row.
  • Jump scrolling and transitions follow the theme's motion setting, including reduced motion.
  • The caller supplies unique ids, meaningful labels, and semantic headings in the reading content.

Examples

Track a page

import { ScrollProgress } from "@/components/ui/scroll-progress";

export default function Guide() {
  return (
    <>
      <ScrollProgress sections={[{ id: "overview", label: "Overview" }, { id: "details", label: "Details" }]} />
      <section id="overview"><h2>Overview</h2><p>Start with the essentials.</p></section>
      <section id="details"><h2>Details</h2><p>Explore the next steps.</p></section>
    </>
  );
}

Track a scroll container

"use client";
import { useRef } from "react";
import { ScrollProgress } from "@/components/ui/scroll-progress";

export default function ReadingPanel() {
  const containerRef = useRef<HTMLDivElement>(null);
  return (
    <div className="relative">
      <div ref={containerRef} className="h-96 overflow-y-auto pb-20" tabIndex={0} role="region" aria-label="Guide">
        <section id="start" className="min-h-96"><h2>Getting started</h2></section>
        <section id="next" className="min-h-96"><h2>Next steps</h2></section>
      </div>
      <ScrollProgress className="absolute" containerRef={containerRef} sections={[{ id: "start", label: "Getting started" }, { id: "next", label: "Next steps" }]} />
    </div>
  );
}