Scroll Progress

A progress bar that follows the scroll position of the page or of a container.

<script setup lang="ts">
import { ArrowDown02Icon } from '@hugeicons/core-free-icons'
import { HugeiconsIcon } from '@hugeicons/vue'
import {
  ScrollProgress,
  ScrollProgressContainer,
  ScrollProgressProvider,
} from '@animate-ui/vue/primitives/scroll-progress'
import { cn } from 'cn'
import { motion } from 'motion-v'
import { computed } from 'vue'

const props = withDefaults(
  defineProps<{
    global?: boolean | string
    direction?: 'horizontal' | 'vertical'
  }>(),
  { global: false, direction: 'vertical' },
)

const isGlobal = computed(() => props.global === true || props.global === 'true')
const horizontal = computed(() => props.direction === 'horizontal')
</script>

<template>
  <div class="relative h-72 w-full max-w-md overflow-hidden rounded-xl border">
    <ScrollProgressProvider
      :key="`${isGlobal}-${direction}`"
      :global="isGlobal"
      :direction="direction"
    >
      <div
        :class="cn('z-50', isGlobal ? 'fixed inset-x-0 top-0' : 'absolute inset-x-3 bottom-3')"
      >
        <ScrollProgress :class="cn('h-1.5 bg-foreground', !isGlobal && 'rounded-full')" />
      </div>
      <div v-if="isGlobal" class="flex size-full items-center justify-center">
        <p class="text-sm font-medium">
          Scroll the page to see the progress bar
        </p>
      </div>
      <ScrollProgressContainer
        v-else
        :class="cn('size-full', horizontal ? 'overflow-x-auto' : 'overflow-y-auto')"
      >
        <div :class="cn('flex', !horizontal && 'flex-col')">
          <div class="flex h-72 w-full shrink-0 items-center justify-center">
            <p class="flex items-center gap-2 text-sm font-medium">
              Scroll to see the progress bar
              <motion.span
                :class="horizontal ? '-rotate-90' : undefined"
                :animate="{ y: [3, -3, 3] }"
                :transition="{ duration: 1.25, repeat: Infinity, ease: 'easeInOut', type: 'keyframes' }"
              >
                <HugeiconsIcon :icon="ArrowDown02Icon" :stroke-width="2" class="size-5" />
              </motion.span>
            </p>
          </div>
          <div class="h-72 w-full shrink-0 p-3">
            <div class="size-full rounded-xl bg-accent" />
          </div>
          <div class="h-72 w-full shrink-0" />
          <div class="h-72 w-full shrink-0 p-3">
            <div class="size-full rounded-xl bg-accent" />
          </div>
          <div class="h-72 w-full shrink-0" />
        </div>
      </ScrollProgressContainer>
    </ScrollProgressProvider>
  </div>
</template>

Installation

npm install @animate-ui/vue motion-v
pnpm add @animate-ui/vue motion-v
yarn add @animate-ui/vue motion-v
bun add @animate-ui/vue motion-v

Usage

<script setup lang="ts">
import {
  ScrollProgress,
  ScrollProgressContainer,
  ScrollProgressProvider,
} from '@animate-ui/vue/primitives/scroll-progress';
</script>

<template>
  <!-- A scrollable container. -->
  <ScrollProgressProvider>
    <ScrollProgress class="h-1 bg-foreground" />
    <ScrollProgressContainer class="h-96 overflow-y-auto">
      …
    </ScrollProgressContainer>
  </ScrollProgressProvider>

  <!-- The whole page. -->
  <ScrollProgressProvider global>
    <ScrollProgress class="fixed inset-x-0 top-0 h-1 bg-foreground" />
  </ScrollProgressProvider>
</template>

Animation

  • The bar follows the scroll position on a spring without overshoot (stiffness 250, damping 40), so it glides instead of jumping on wheel steps.
  • By default it grows in width (mode="width", from 0 to 100%), so rounded ends keep their shape; height does the same vertically. scaleX and scaleY scale it with a transform instead, from its transform-origin (the center unless you set one, e.g. origin-left).
  • Scrolling never re-renders Vue: the bar is driven by motion values.
  • Under reduced motion, the bar tracks the scroll position without easing.

API Reference

ScrollProgressProvider

PropTypeDefault
global?
booleanfalse
direction?
'vertical' | 'horizontal''vertical'
transition?
SpringOptions{ stiffness: 250, damping: 40, bounce: 0 }
#default
slot-

ScrollProgress

PropTypeDefault
as-child?
booleanfalse
mode?
'width' | 'height' | 'scaleX' | 'scaleY''width'
...props?
HTMLMotionProps<'div'>-

ScrollProgressContainer

The scrollable element (without global). Make it scroll with overflow-y-auto.

PropTypeDefault
as-child?
booleanfalse
...props?
HTMLMotionProps<'div'>-

useScrollProgress

Returns getters for the progress (raw, 0 to 1) and scale (smoothed) motion values, direction and global, with the container ref.

Built by Skyleen. The source code is available on GitHub.

Last updated: 10/23/2026