---
title: Scroll Progress
description: "Composable page progress: a line, section rail, liquid dots, or an orbit."
seo:
    title: "Scroll Progress for Astro — examples and usage"
---

Scroll through the examples below. Each indicator stays in view while its
section scrolls from start to finish. The next example then takes its place.
You can also [open the separate full page demo](/examples/scroll-progress).

## Composition

`ScrollProgress` is a polymorphic HTML container. It renders a `div` by default,
or the tag given with `as`. It does not use a custom element or a client framework.
With no children, it supplies a track and fill:

```astro
<ScrollProgress />
<ScrollProgress orientation="horizontal" />
```

The default vertical line sits at the right edge. The horizontal line sits at
the top. Both track vertical scroll distance, from 0 at the top to 1 at the bottom.
A page with no scroll range stays at 0.

The previews use `scrollTarget="html"` and a `progressTarget` section. A sticky
wrapper keeps the indicator visible during that section.

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

<section data-progress-section class="relative min-h-[160vh] w-full">
  <div class="sticky top-24 flex min-h-[60vh] flex-col items-center justify-center gap-8 rounded-xl border bg-background p-6">
    <div class="text-center">
      <p class="font-medium">Line</p>
      <p class="text-sm text-muted-foreground">
        Scroll through this section to fill the indicator.
      </p>
    </div>
    <ScrollProgress
      scrollTarget="html"
      progressTarget="[data-progress-section]"
      position="relative"
      aria-hidden="true"
    />
    <p class="text-xs text-muted-foreground">
      0% at the start · 100% at the end
    </p>
  </div>
</section>
```

## Tracks and fills

Use parts when you need more control. Each part accepts `class`, `style`, native
HTML attributes, and a slot. The root, track, fill, and indicator also accept `as`.

```astro
---
import {
  ScrollProgress,
  ScrollProgressTrack,
  ScrollProgressFill,
} from "@/components/ui/scroll-progress"
---

<ScrollProgress orientation="horizontal">
  <ScrollProgressTrack class="h-1!">
    <ScrollProgressFill />
  </ScrollProgressTrack>
</ScrollProgress>
```

`ScrollProgressTrack` supplies the dimensions and background. Its variants are
`line`, `rail`, `gooey`, and `orbit`. `ScrollProgressFill` supplies a growing line
or an SVG ring with `variant="orbit"`. You can omit the fill.

The `--scroll-progress` CSS property is a number from 0 to 1. Custom content can
use it to draw a different shape without another scroll script.

## Indicators

`ScrollProgressIndicator` is optional. The default `rail` variant groups section
items. The `gooey` variant supplies four liquid dots and a moving blob. The
`orbit` variant supplies a dot that travels around the ring. A slot replaces
its default content, so you can supply your own marks or SVG.

### Gooey

```astro
---
import {
  ScrollProgress,
  ScrollProgressTrack,
  ScrollProgressIndicator,
} from "@/components/ui/scroll-progress"
---

<section data-progress-section class="relative min-h-[160vh] w-full">
  <div class="sticky top-24 flex min-h-[60vh] flex-col items-center justify-center gap-8 rounded-xl border bg-background p-6">
    <div class="text-center">
      <p class="font-medium">Gooey</p>
      <p class="text-sm text-muted-foreground">
        Scroll through this section to fill the indicator.
      </p>
    </div>
    <ScrollProgress
      scrollTarget="html"
      progressTarget="[data-progress-section]"
      position="relative"
      aria-hidden="true"
    >
      <ScrollProgressTrack variant="gooey">
        <ScrollProgressIndicator variant="gooey" />
      </ScrollProgressTrack>
    </ScrollProgress>
    <p class="text-xs text-muted-foreground">
      0% at the start · 100% at the end
    </p>
  </div>
</section>
```

### Orbit

The ring and orbiting dot are separate parts. Omit the indicator for a plain ring.

```astro
---
import {
  ScrollProgress,
  ScrollProgressTrack,
  ScrollProgressFill,
  ScrollProgressIndicator,
} from "@/components/ui/scroll-progress"
---

<section data-progress-section class="relative min-h-[160vh] w-full">
  <div class="sticky top-24 flex min-h-[60vh] flex-col items-center justify-center gap-8 rounded-xl border bg-background p-6">
    <div class="text-center">
      <p class="font-medium">Orbit</p>
      <p class="text-sm text-muted-foreground">
        Scroll through this section to fill the indicator.
      </p>
    </div>
    <ScrollProgress
      scrollTarget="html"
      progressTarget="[data-progress-section]"
      position="relative"
      aria-hidden="true"
    >
      <ScrollProgressTrack variant="orbit">
        <ScrollProgressFill variant="orbit" />
        <ScrollProgressIndicator variant="orbit" />
      </ScrollProgressTrack>
    </ScrollProgress>
    <p class="text-xs text-muted-foreground">
      0% at the start · 100% at the end
    </p>
  </div>
</section>
```

### Horizontal

Orientation changes the layout, not the scroll axis. A horizontal line or a row
of dots can show progress through the same vertical page.

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

<section data-progress-section class="relative min-h-[160vh] w-full">
  <div class="sticky top-24 flex min-h-[60vh] flex-col items-center justify-center gap-8 rounded-xl border bg-background p-6">
    <div class="text-center">
      <p class="font-medium">Horizontal</p>
      <p class="text-sm text-muted-foreground">
        Scroll through this section to fill the indicator.
      </p>
    </div>
    <ScrollProgress
      scrollTarget="html"
      progressTarget="[data-progress-section]"
      orientation="horizontal"
      position="relative"
      class="w-full max-w-64"
      aria-hidden="true"
    />
    <p class="text-xs text-muted-foreground">
      0% at the start · 100% at the end
    </p>
  </div>
</section>
```

## Section links

Use `as="nav"` and give the navigation an accessible name. Items are native
anchors. Use `href="#section-id"` to refer to a section; `id` remains the native
ID of the link itself. This avoids duplicate IDs on the link and its target.

```astro
---
import {
  ScrollProgress,
  ScrollProgressTrack,
  ScrollProgressIndicator,
  ScrollProgressItem,
} from "@/components/ui/scroll-progress"

const items = [
  { id: "about", label: "About" },
  { id: "work", label: "Work" },
]
---

<ScrollProgress as="nav" aria-label="Page sections" orientation="vertical">
  <ScrollProgressTrack variant="rail">
    <ScrollProgressIndicator>
      {items.map(item => (
        <ScrollProgressItem href={`#${item.id}`}>
          {item.label}
        </ScrollProgressItem>
      ))}
    </ScrollProgressIndicator>
  </ScrollProgressTrack>
</ScrollProgress>

<section id="about">...</section>
<section id="work">...</section>
```

Keep items in document order. Content can be text, an icon, or both. A current,
hovered, or focused item shows its content and expands its mark. To omit visible
labels, leave the slot empty and supply an `aria-label`:

```astro
---
import {
  ScrollProgress,
  ScrollProgressTrack,
  ScrollProgressIndicator,
  ScrollProgressItem,
} from "@/components/ui/scroll-progress"
const id = `rail-${crypto.randomUUID()}`
const sections = ["Start", "Middle", "Finish"]
---

<section
  data-progress-section
  class="relative grid w-full grid-cols-[auto_1fr] gap-6"
>
  <div class="relative">
    <ScrollProgress
      as="nav"
      aria-label="Demo sections"
      scrollTarget="html"
      progressTarget="[data-progress-section]"
      position="relative"
      class="sticky top-24"
    >
      <ScrollProgressTrack variant="rail">
        <ScrollProgressIndicator>
          {sections.map((label, i) => (
            <ScrollProgressItem href={`#${id}-${i}`} aria-label={label}>
              {label}
            </ScrollProgressItem>
          ))}
        </ScrollProgressIndicator>
      </ScrollProgressTrack>
    </ScrollProgress>
  </div>
  <div>
    {sections.map((label, i) => (
      <section
        id={`${id}-${i}`}
        class="flex min-h-[70vh] scroll-mt-24 items-start border-t py-8"
      >
        <p class="text-sm text-muted-foreground">
          {label} — scroll to the next section.
        </p>
      </section>
    ))}
  </div>
</section>
```

Empty items get a fallback accessible name from their fragment target. Supply a
clear `aria-label` for an icon-only item. No item list, labels, or section IDs
are needed when you only want total progress.

## Position and scroll source

`position="fixed"` keeps the indicator at the viewport edge. Use
`position="absolute"` to anchor it inside a positioned ancestor, or
`position="relative"` to keep it in normal flow. Tailwind classes can set offsets
or change positioning to `sticky`.

The script finds the nearest ancestor with vertical `overflow: auto`, `scroll`,
or `hidden`. If there is none, it tracks the document.
A `relative` class controls the containing block for absolute positioning;
it does not make that element scrollable. Use `overflow-clip` when you need
clipping without a scroll source. Fixed positioning does not change which
ancestor the script tracks.

To select a scroll container yourself, pass a CSS selector to `scrollTarget`:

```astro
<div id="reader" class="h-96 overflow-y-auto">
  <article class="min-h-[72rem]">...</article>
</div>

<ScrollProgress scrollTarget="#reader" />
```

The indicator can be outside the selected container. The selector uses the first
matching HTML element in the document. Section links must point to sections
inside that container. Use `scrollTarget="html"` or `scrollTarget="body"` to
track the page explicitly.

The target must exist when the component initializes. An invalid selector or a
missing target leaves the indicator empty. It does not track a different source.
An omitted or blank selector keeps automatic parent detection.

### Track one section

Use `progressTarget` to measure one section within the scroll container. The
nearest matching ancestor is used first, then the first document match.

```astro
<section data-reading-section class="min-h-[160vh]">
  <div class="sticky top-24">
    <ScrollProgress
      scrollTarget="html"
      progressTarget="[data-reading-section]"
      position="relative"
    />
  </div>
</section>
```

Progress starts when the section top reaches the scroll viewport top. It ends
when the section bottom reaches the viewport bottom. The section must be taller
than the viewport; otherwise progress stays at 0. A missing or invalid target
also stays at 0. Omit `progressTarget` to measure the whole scroll container.

### With Scroll Area

[Scroll Area](/components/scroll-area) works with the same component. Give its
scrolling viewport an ID through `viewportProps`, then use that ID as the target:

```astro
<ScrollArea class="h-80" viewportProps={{ id: "reading-viewport" }}>
  <article>...</article>
</ScrollArea>

<ScrollProgress scrollTarget="#reading-viewport" />
```

The viewport must have enough content to scroll. Target the inner viewport,
not the outer `ScrollArea` wrapper. A `ScrollProgress` placed inside the viewport
can omit `scrollTarget` and use automatic parent detection. Progress tracks the
vertical scroll distance, including when the indicator uses a horizontal layout.

## Behavior and access

One passive scroll listener per instance schedules at most one update per
animation frame. It writes `--scroll-progress`; CSS draws each part. An
`IntersectionObserver` updates section selection as sections cross a reading
line 45% down the viewport. The
first and last links are selected at the scroll boundaries, including a short
final section. Normal scroll updates do not scan section positions.

This implementation uses JavaScript for progress in all browsers. This keeps
page scrolling and nested scrolling on the same path, including fixed elements
inside a scroll container. With JavaScript off, native links still work and the
fill stays empty.

Resize observers refresh the progress when content size changes. The script
removes instance listeners and observers when a root is removed or Astro changes
the page. The fill is decorative and does not announce every scroll update.
Links retain native keyboard and fragment navigation.

A reduced-motion preference removes mark transitions, the gooey filter, and the
orbiting dot. The line and ring still show progress. The component does not set
smooth scrolling.

## Installation

```sh
bunx --bun shadcn@latest add @gnosticai/scroll-progress
```

## API reference

| Root prop        | Values                                    | Default                     |
| ---------------- | ----------------------------------------- | --------------------------- |
| `as`             | An HTML tag                               | `div`                       |
| `orientation`    | `vertical`, `horizontal`                  | `vertical`                  |
| `position`       | `fixed`, `absolute`, `relative`           | `fixed`                     |
| `scrollTarget`   | A CSS selector for the scroll container   | Nearest scrollable ancestor |
| `progressTarget` | A CSS selector for the section to measure | Whole scroll container      |

| Part                      | `variant` values                 | Default |
| ------------------------- | -------------------------------- | ------- |
| `ScrollProgressTrack`     | `line`, `rail`, `gooey`, `orbit` | `line`  |
| `ScrollProgressIndicator` | `rail`, `gooey`, `orbit`         | `rail`  |
| `ScrollProgressFill`      | `line`, `orbit`                  | `line`  |

The track and indicator default to `div`; the fill defaults to `span`. Each
accepts an `as` prop. `ScrollProgressItem` is an anchor with a required `href`
fragment, optional slot content, and native anchor attributes.

## Theme and layout

Parts use the primary, foreground, background, and border tokens. Use Tailwind
classes to set dimensions, offsets, and colors. Leave room beside content for
fixed section links. Keep the rail short enough to fit on the smallest screen.

The root, track, fill, and indicator classes use CVA. Their variant functions and
inferred types are exported from the component index, as with Button and Badge.

The earlier `items`, `variant`, and `source` props on the root have been replaced
by slots, part variants, and automatic source detection with an optional
`scrollTarget` selector.

## Related choices

Use [Progress](/components/progress) for task completion and
[Scroll Area](/components/scroll-area) for a bounded native scroll panel.
The section rail takes its direction from [Albina Nikiforova's site](https://albinanikiforova.com/).
