@todovue/tv-progress-bar
v1.1.0
Published
A simple and customizable progress bar component for Vue.js applications.
Downloads
158
Maintainers
Readme
TODOvue Progress Bar (TvProgressBar)
A lightweight, customizable Vue 3 reading progress bar component that tracks scroll position through content. Features smooth animations, flexible target selection, configurable offsets, and SSR support. Works seamlessly in Single Page Apps and Server-Side Rendered (SSR) environments like Nuxt 3.
Demo: https://ui.todovue.blog/progressbar
Table of Contents
- Features
- Installation
- Quick Start (SPA)
- Nuxt 4 / SSR Usage
- Component Registration Options
- Props
- Composable API
- Usage Examples
- Accessibility
- SSR Notes
- Development
- Contributing
- License
Features
- Real-time reading progress tracking based on scroll position
- Flexible target selection (CSS selector, element reference, or DOM element)
- Configurable height and color
- Support for Gradients: Pass multiple colors for a modern look
- Glow Effect: Optional neon glow that follows the progress bar
- Customizable Transitions: Configure duration and easing functions
- Vertical Orientation: Support for side progress bars (left/right)
- Reading Checkpoints: Display indicators at specific progress points (e.g., 25%, 50%, 75%)
- Progress Labels: Show percentage inside the bar or as a floating bubble
- Flexible Positioning: Fix the bar at the top, bottom, left, right, or use sticky behavior
- Top and bottom offset support for fixed headers/footers
- Smooth linear transitions with reduced motion support
- SSR-safe (works with Nuxt 3 and other SSR frameworks)
- Composable API (
useProgressBar) for custom implementations - ResizeObserver support for responsive content
- RequestAnimationFrame optimization for smooth performance
- Keyboard accessible with ARIA labels
- Lightweight and tree-shakeable
- TypeScript support
Installation
Using npm:
npm install @todovue/tv-progress-barUsing yarn:
yarn add @todovue/tv-progress-barUsing pnpm:
pnpm add @todovue/tv-progress-barImporting Styles
Important: You must explicitly import the stylesheet in your application.
For Vue/Vite SPA:
// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import '@todovue/tv-progress-bar/style.css'
import { TvProgressBar } from '@todovue/tv-progress-bar'
const app = createApp(App)
app.component('TvProgressBar', TvProgressBar)
app.mount('#app')For Nuxt 3/4:
// nuxt.config.ts
export default defineNuxtConfig({
modules: [
'@todovue/tv-progress-bar/nuxt'
]
})Then register the component in a plugin as shown in the Nuxt 3 / SSR Usage section.
Quick Start (SPA)
Global registration (main.js / main.ts):
import { createApp } from 'vue'
import App from './App.vue'
import '@todovue/tv-progress-bar/style.css'
import TvProgressBar from '@todovue/tv-progress-bar'
createApp(App)
.use(TvProgressBar) // enables <TvProgressBar /> globally
.mount('#app')Local import inside a component:
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
</script>
<template>
<div>
<!-- Progress bar tracks the article container -->
<TvProgressBar :target="articleContainer" />
<!-- Your article content -->
<article ref="articleContainer">
<h1>My Article</h1>
<p>Lorem ipsum dolor sit amet...</p>
<!-- Long content here -->
</article>
</div>
</template>Note: Don't forget to import the CSS in your main entry file as shown above.
Nuxt 4 / SSR Usage
First, add the module to your nuxt.config.ts:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@todovue/tv-progress-bar/nuxt']
})Alternatively, you can manually add the CSS:
// nuxt.config.ts
export default defineNuxtConfig({
css: ['@todovue/tv-progress-bar/style.css'],
})Then create a plugin file: plugins/tv-progress-bar.client.ts:
import { defineNuxtPlugin } from '#app'
import TvProgressBar from '@todovue/tv-progress-bar'
export default defineNuxtPlugin(nuxtApp => {
nuxtApp.vueApp.use(TvProgressBar)
})Use anywhere in your Nuxt app:
<script setup>
import { ref } from 'vue'
const mainContent = ref(null)
</script>
<template>
<div>
<TvProgressBar :target="mainContent" :offset-top="60" />
<main ref="mainContent">
<NuxtPage />
</main>
</div>
</template>Optional direct import (no plugin):
<script setup>
import { TvProgressBar } from '@todovue/tv-progress-bar'
</script>Component Registration Options
| Approach | When to use |
|------------------------------------------------------------------------------|------------------------------------------------|
| Global via app.use(TvProgressBar) | Many usages across app / design system install |
| Local named import { TvProgressBar } | Isolated / code-split contexts |
| Direct default import import TvProgressBar from '@todovue/tv-progress-bar' | Single usage or manual registration |
Props
| Prop | Type | Default | Description | |---------------|------------------|--------------------------------------------------|-----------------------------------------------------------------| | target | String | Object | '.container-blog' | CSS selector or element reference to track scroll progress. | | offsetTop | Number | 0 | Top offset in pixels (useful for fixed headers). | | offsetBottom | Number | 0 | Bottom offset in pixels (useful for fixed footers). | | height | String | '4px' | Height of horizontal progress bar (CSS value). | | width | String | '4px' | Width of vertical progress bar (CSS value). | | zIndex | Number | 1200 | Z-index for the progress bar positioning. | | disabled | Boolean | false | Whether the progress bar is enabled and visible. | | color | String | '' | Custom background color for the progress bar (CSS color value). | | gradient | Array | [] | Array of colors for a linear gradient background. | | glow | Boolean | false | Whether to enable the glow effect. | | glowColor | String | '' | Custom color for the glow effect. | | duration | String | '120ms' | Transition duration (e.g., '300ms', '0.5s'). | | easing | String | 'linear' | Transition easing function (e.g., 'ease-in-out'). | | orientation | String | 'horizontal' | Bar orientation: 'horizontal' or 'vertical'. | | position | String | 'top' (horiz) / 'left' (vert) | Positioning: 'top', 'bottom', 'left', 'right', or 'sticky'. | | showLabel | Boolean | false | Whether to show the percentage label. | | labelPosition | String | 'inside' | Label position: 'inside' or 'floating'. | | checkpoints | Array | [] | Array of numbers (0-100) to show indicators on the bar. |
Prop Details
target
The element to track for reading progress. Can be:
- A CSS selector string (e.g.,
'.article','#content') - A template ref (e.g.,
ref(null)) - An HTMLElement reference
Example:
<script setup>
import { ref } from 'vue'
const articleRef = ref(null)
</script>
<template>
<!-- Using template ref -->
<TvProgressBar :target="articleRef" />
<article ref="articleRef">...</article>
<!-- Using CSS selector -->
<TvProgressBar target="#my-article" />
<article id="my-article">...</article>
</template>offsetTop
Accounts for fixed headers or navigation bars at the top of the page. The progress calculation will consider this offset.
Example:
<!-- 60px offset for fixed header -->
<TvProgressBar :target="articleRef" :offset-top="60" />offsetBottom
Accounts for fixed footers or bottom bars. The progress calculation will consider this offset.
Example:
<!-- 80px offset for fixed footer -->
<TvProgressBar :target="articleRef" :offset-bottom="80" />height
Controls the thickness of the progress bar. Accepts any CSS height value.
Example:
<TvProgressBar :target="articleRef" height="8px" />
<TvProgressBar :target="articleRef" height="0.5rem" />zIndex
Controls the stacking order of the progress bar. Default is 1200 to ensure it appears above most content.
Example:
<TvProgressBar :target="articleRef" :z-index="9999" />disabled
Allows you to conditionally enable/disable the progress bar. When disabled, the bar won't be rendered.
Example:
<script setup>
import { ref } from 'vue'
const showProgress = ref(true)
</script>
<template>
<TvProgressBar :target="articleRef" :disabled="showProgress" />
</template>color
Custom color for the progress bar. Accepts any CSS color value. If not provided, uses the default theme color.
Example:
<TvProgressBar :target="articleRef" color="#42b983" />
<TvProgressBar :target="articleRef" color="rgb(66, 185, 131)" />
<TvProgressBar :target="articleRef" color="var(--primary-color)" />gradient
Array of colors to create a linear gradient background. When provided, it overrides the color prop.
Example:
<TvProgressBar :target="articleRef" :gradient="['#f093fb', '#f5576c']" />
<TvProgressBar :target="articleRef" :gradient="['#84fab0', '#8fd3f4']" />glow
Enables a shadow effect that follows the progress bar, giving it a depth or "neon" look.
Example:
<TvProgressBar :target="articleRef" glow />
<TvProgressBar :target="articleRef" color="#00f2fe" glow />glowColor
Customizes the color of the glow effect. If not provided, it defaults to the color prop or the last color in the gradient.
Example:
<TvProgressBar :target="articleRef" glow glow-color="#ff00ff" />duration
Sets the duration of the progress bar transition.
Example:
<TvProgressBar :target="articleRef" duration="300ms" />
<TvProgressBar :target="articleRef" duration="0.5s" />easing
Sets the easing function for the progress bar transition.
Example:
<TvProgressBar :target="articleRef" easing="ease-in-out" />
<TvProgressBar :target="articleRef" easing="cubic-bezier(0.4, 0, 0.2, 1)" />Composable API
TvProgressBar includes a composable useProgressBar that you can use to build custom progress tracking functionality.
useProgressBar(targetEl, options)
import { useProgressBar } from '@todovue/tv-progress-bar'
const targetEl = ref(null)
const { progress, progressPercent, recalculate } = useProgressBar(targetEl, {
offsetTop: computed(() => 60),
offsetBottom: computed(() => 0)
})Parameters:
targetEl(Ref): Reactive reference to the element to trackoptions(Object): Configuration optionsoffsetTop(Number|Ref|ComputedRef): Top offset in pixels (default: 0)offsetBottom(Number|Ref|ComputedRef): Bottom offset in pixels (default: 0)
Returns:
progress(Ref): Reactive number between 0 and 1 representing scroll progressprogressPercent(Ref): Reactive number between 0 and 100 (rounded)recalculate(Function): Function to manually trigger a progress recalculation
Example:
<script setup>
import { ref, computed } from 'vue'
import { useProgressBar } from '@todovue/tv-progress-bar'
const articleRef = ref(null)
const { progress, progressPercent, recalculate } = useProgressBar(articleRef, {
offsetTop: computed(() => 60),
offsetBottom: computed(() => 0)
})
</script>
<template>
<div>
<!-- Custom progress display -->
<div class="custom-progress">
Reading progress: {{ progressPercent }}%
</div>
<!-- Custom progress bar -->
<div class="custom-bar">
<div
class="custom-fill"
:style="{ width: `${progress * 100}%` }"
/>
</div>
<article ref="articleRef">
<!-- Your content -->
</article>
<button @click="recalculate">Recalculate Progress</button>
</div>
</template>
<style scoped>
.custom-progress {
position: fixed;
top: 10px;
right: 10px;
background: rgba(0, 0, 0, 0.7);
color: white;
padding: 8px 12px;
border-radius: 4px;
font-size: 14px;
}
.custom-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
height: 4px;
background: rgba(0, 0, 0, 0.1);
}
.custom-fill {
height: 100%;
background: #42b983;
transition: width 120ms linear;
}
</style>Usage Examples
Default (CSS Selector)
<script setup>
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
</script>
<template>
<div>
<TvProgressBar target=".article-content" />
<article class="article-content">
<h1>My Article</h1>
<p>Lorem ipsum dolor sit amet...</p>
<!-- Your long content -->
</article>
</div>
</template>Using Template Ref
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
</script>
<template>
<div>
<TvProgressBar :target="articleContainer" />
<article ref="articleContainer">
<h1>My Article</h1>
<p>Lorem ipsum dolor sit amet...</p>
<!-- Your long content -->
</article>
</div>
</template>Custom Color
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
</script>
<template>
<div>
<TvProgressBar
:target="articleContainer"
color="#42b983"
/>
<article ref="articleContainer">
<h1>My Article</h1>
<p>Lorem ipsum dolor sit amet...</p>
</article>
</div>
</template>Custom Height
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
</script>
<template>
<div>
<TvProgressBar
:target="articleContainer"
height="8px"
/>
<article ref="articleContainer">
<h1>My Article</h1>
<p>Lorem ipsum dolor sit amet...</p>
</article>
</div>
</template>Gradient Support
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
</script>
<template>
<div>
<TvProgressBar
:target="articleContainer"
:gradient="['#f093fb', '#f5576c']"
height="6px"
/>
<article ref="articleContainer">
<h1>My Article with Gradient</h1>
<p>Scroll to see the gradient progress bar...</p>
</article>
</div>
</template>Glow Effect
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
</script>
<template>
<div>
<TvProgressBar
:target="articleContainer"
color="#00f2fe"
glow
height="4px"
/>
<article ref="articleContainer">
<h1>Neon Glow Progress</h1>
<p>Notice the subtle glow under the bar...</p>
</article>
</div>
</template>Custom Transitions
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
</script>
<template>
<div>
<TvProgressBar
:target="articleContainer"
duration="800ms"
easing="cubic-bezier(0.68, -0.55, 0.265, 1.55)"
color="#3f51b5"
/>
<article ref="articleContainer">
<h1>Bouncy Progress Bar</h1>
<p>Scroll fast to see the custom easing effect...</p>
</article>
</div>
</template>
### Vertical Orientation
```vue
<template>
<TvProgressBar
target=".content"
orientation="vertical"
position="left"
width="6px"
color="#4f46e5"
/>
</template>Reading Checkpoints
<template>
<TvProgressBar
target=".content"
:checkpoints="[25, 50, 75]"
color="#f59e0b"
/>
</template>Floating Percentage Label
<template>
<TvProgressBar
target=".content"
show-label
label-position="floating"
color="#10b981"
glow
/>
</template>Sticky Position (Inside Container)
<template>
<div class="relative-container">
<TvProgressBar
target=".content"
position="sticky"
color="#ec4899"
/>
<div class="content">...</div>
</div>
</template>
### With Fixed Header (Offset Top)
```vue
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
</script>
<template>
<div>
<header style="position: fixed; top: 0; height: 60px;">
<!-- Fixed header content -->
</header>
<TvProgressBar
:target="articleContainer"
:offset-top="60"
/>
<article ref="articleContainer" style="margin-top: 60px;">
<h1>My Article</h1>
<p>Lorem ipsum dolor sit amet...</p>
</article>
</div>
</template>With Fixed Header and Footer
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
</script>
<template>
<div>
<header style="position: fixed; top: 0; height: 60px;">
<!-- Fixed header -->
</header>
<TvProgressBar
:target="articleContainer"
:offset-top="60"
:offset-bottom="80"
/>
<article ref="articleContainer" style="margin: 60px 0 80px;">
<h1>My Article</h1>
<p>Lorem ipsum dolor sit amet...</p>
</article>
<footer style="position: fixed; bottom: 0; height: 80px;">
<!-- Fixed footer -->
</footer>
</div>
</template>Conditional Rendering
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
const showProgress = ref(true)
</script>
<template>
<div>
<button @click="showProgress = !showProgress">
Toggle Progress Bar
</button>
<TvProgressBar
:target="articleContainer"
:disabled="showProgress"
/>
<article ref="articleContainer">
<h1>My Article</h1>
<p>Lorem ipsum dolor sit amet...</p>
</article>
</div>
</template>All Props Combined
<script setup>
import { ref } from 'vue'
import { TvProgressBar } from '@todovue/tv-progress-bar'
import '@todovue/tv-progress-bar/style.css'
const articleContainer = ref(null)
</script>
<template>
<div>
<TvProgressBar
:target="articleContainer"
:offset-top="60"
:offset-bottom="40"
height="6px"
color="#ff6b6b"
:z-index="9999"
disabled
/>
<article ref="articleContainer">
<h1>My Article</h1>
<p>Lorem ipsum dolor sit amet...</p>
</article>
</div>
</template>Custom Implementation with Composable
<script setup>
import { ref, computed } from 'vue'
import { useProgressBar } from '@todovue/tv-progress-bar'
const contentRef = ref(null)
const { progress, progressPercent, recalculate } = useProgressBar(contentRef, {
offsetTop: computed(() => 0),
offsetBottom: computed(() => 0)
})
</script>
<template>
<div>
<!-- Circular progress indicator -->
<div class="circular-progress">
<svg width="60" height="60">
<circle
cx="30"
cy="30"
r="25"
fill="none"
stroke="#e0e0e0"
stroke-width="5"
/>
<circle
cx="30"
cy="30"
r="25"
fill="none"
stroke="#42b983"
stroke-width="5"
:stroke-dasharray="`${progress * 157} 157`"
transform="rotate(-90 30 30)"
/>
</svg>
<span class="percentage">{{ progressPercent }}%</span>
</div>
<article ref="contentRef">
<h1>Custom Progress Indicator</h1>
<p>Scroll to see the circular progress...</p>
<!-- Your content -->
</article>
</div>
</template>
<style scoped>
.circular-progress {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
align-items: center;
justify-content: center;
}
.percentage {
position: absolute;
font-size: 12px;
font-weight: bold;
color: #42b983;
}
</style>Accessibility
- ARIA Attributes: Progress bar includes proper ARIA attributes:
role="progressbar"aria-label="Reading progress"aria-valuemin="0"aria-valuemax="100"aria-valuenow(dynamically updated percentage)
- Pointer Events: Progress bar uses
pointer-events: noneto not interfere with page interactions - Reduced Motion: Respects
prefers-reduced-motionmedia query to disable transitions for users who prefer reduced motion - Semantic HTML: Uses semantic div elements with proper ARIA roles
- Visual Feedback: Clear visual indication of reading progress
SSR Notes
- SSR-Safe: No direct
window/documentaccess during module evaluation - Smart Guards: Uses
typeof window !== 'undefined'checks throughout - Lifecycle Hooks: Scroll listeners and observers are added in
onMountedhook - Cleanup: Automatically removes event listeners and observers in
onBeforeUnmount - Nuxt 3 Compatible: Works seamlessly with Nuxt 3 out of the box
- Hydration Safe: No hydration mismatches
- Performance: Uses
requestAnimationFramefor smooth updates - ResizeObserver: Automatically recalculates on content resize
- Passive Listeners: Scroll event listeners use
passive: truefor better performance
Development
git clone https://github.com/TODOvue/tv-progress-bar.git
cd tv-progress-bar
npm install
npm run dev # run demo playground
npm run build # build libraryLocal demo served from Vite using index.html and demo examples in src/demo.
Contributing
PRs and issues welcome. See CONTRIBUTING.md and CODE_OF_CONDUCT.md.
License
MIT © TODOvue
Attributions
Crafted for the TODOvue component ecosystem
