Progression
A progression component to display scroll progress through content.
Features
- Scroll-based progress tracking
- Circular and bar progress indicators
- Smooth animations and transitions
- Automatic content measurement
- Programmatic scroll control
- Responsive design
Long Content Example
This page demonstrates how the progression components track scroll progress through content. The circle and bar indicators above will update as you scroll through this long article.
Section 1
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
Key Point 1
Important information that demonstrates the content structure and layout.
Key Point 2
Additional content to show how the progression works with longer sections.
Section 2
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
Key Point 2
Important information that demonstrates the content structure and layout.
Key Point 3
Additional content to show how the progression works with longer sections.
Section 3
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
Key Point 3
Important information that demonstrates the content structure and layout.
Key Point 4
Additional content to show how the progression works with longer sections.
Section 4
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
Key Point 4
Important information that demonstrates the content structure and layout.
Key Point 5
Additional content to show how the progression works with longer sections.
Section 5
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.
Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium.
Key Point 5
Important information that demonstrates the content structure and layout.
Key Point 6
Additional content to show how the progression works with longer sections.
End of Content
You've reached the end! The progression indicators should now show 100%.
Installation
Install the following dependencies:
We use @radix-ui/react-slot
for component composition.
Make sure that the following shadcn/ui components are present in your project:
Copy and paste the following code into your project.
'use client';
import { cn } from '@kit/shared';
import { Slot } from '@radix-ui/react-slot';
import React, { createContext, SVGAttributes, useContext, useEffect, useRef, useState } from 'react';
interface ProgressionContextType {
progress: number; // 0-100
progressRef: React.RefObject<number>;
isReady: boolean;
scrollToProgress: (progress: number) => void;
updateProgress: (progress: number) => void;
setReady: (ready: boolean) => void;
setScrollToProgressFn: (fn: (progress: number) => void) => void;
}
const ProgressionContext = createContext<ProgressionContextType>({
progress: 0,
progressRef: { current: 0 },
isReady: false,
scrollToProgress: () => {},
updateProgress: () => {},
setReady: () => {},
setScrollToProgressFn: () => {},
});
function useProgression() {
const context = useContext(ProgressionContext);
if (!context) {
throw new Error('Progression must be used within a ProgressionProvider');
}
return context;
}
/**
* Root component that provides the progression context
*/
function ProgressionRoot({ children }: { children: React.ReactNode }) {
const [progress, setProgress] = useState(0);
const [isReady, setIsReady] = useState(false);
const [scrollToProgressFn, setScrollToProgressFn] = useState<((progress: number) => void) | null>(null);
const progressRef = useRef<number>(0);
// Function to scroll to a specific progress percentage
const scrollToProgress = React.useCallback(
(targetProgress: number) => {
if (scrollToProgressFn) {
scrollToProgressFn(targetProgress);
} else {
console.warn('scrollToProgress called but no content measured yet');
}
},
[scrollToProgressFn]
);
const updateProgress = React.useCallback((newProgress: number) => {
progressRef.current = newProgress;
setProgress(newProgress);
}, []);
const contextValue: ProgressionContextType = {
progress,
progressRef: progressRef,
isReady,
scrollToProgress,
updateProgress,
setReady: setIsReady,
setScrollToProgressFn,
};
return <ProgressionContext.Provider value={contextValue}>{children}</ProgressionContext.Provider>;
}
export interface ProgressionContentProps {
children: React.ReactNode;
/**
* @default false
*/
asChild?: boolean;
className?: string;
style?: React.CSSProperties;
}
/**
* Component that wraps the content to measure scroll progression
*/
function ProgressionContent({ children, asChild = false, className, style }: ProgressionContentProps) {
const contentRef = useRef<HTMLDivElement>(null);
const { updateProgress, setReady, setScrollToProgressFn, progressRef } = useProgression();
const Comp = asChild ? Slot : 'div';
// Measure content and update progress
const measureProgress = React.useCallback(() => {
if (!contentRef.current) return;
const element = contentRef.current;
const rect = element.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const windowHeight = window.innerHeight;
// Calculate total scrollable height
const totalHeight = rect.height;
const startPosition = rect.top + scrollTop;
// Calculate current scroll position relative to content
const currentPosition = Math.max(
0,
Math.min(scrollTop - startPosition + (progressRef.current / 100) * windowHeight)
);
// Calculate progress as percentage
const newProgress =
totalHeight > 0 ? Math.min(100, Math.max(0, (currentPosition / totalHeight) * 100)) : 0;
updateProgress(newProgress);
// Mark as ready once we have measurements
if (totalHeight > 0) {
setReady(true);
}
}, [updateProgress, setReady]);
// Function to scroll to a specific progress percentage
const scrollToProgress = React.useCallback((targetProgress: number) => {
if (!contentRef.current) return;
const element = contentRef.current;
const rect = element.getBoundingClientRect();
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
const startPosition = rect.top + scrollTop;
// Calculate target scroll position
const targetScrollTop = startPosition + (rect.height * targetProgress) / 100 - window.innerHeight / 2;
window.scrollTo({
top: Math.max(0, targetScrollTop),
behavior: 'smooth',
});
}, []);
// Set the scroll function in context
useEffect(() => {
setScrollToProgressFn(() => scrollToProgress);
}, [scrollToProgress, setScrollToProgressFn]);
// Handle scroll events
useEffect(() => {
let rafId: number | null = null;
const handleScroll = () => {
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
rafId = requestAnimationFrame(measureProgress);
};
const handleResize = () => {
measureProgress();
};
// Initial measurement
measureProgress();
window.addEventListener('scroll', handleScroll, { passive: true });
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('scroll', handleScroll);
window.removeEventListener('resize', handleResize);
if (rafId !== null) {
cancelAnimationFrame(rafId);
}
};
}, [measureProgress]);
return (
<Comp ref={contentRef} className={className} style={style}>
{children}
</Comp>
);
}
export interface ProgressionCircleProps {
/**
* Size of the circle
* @default 40
*/
size?: number;
/**
* Width of the stroke
* @default 2
*/
strokeWidth?: number;
/**
* Line cap of the stroke
* @default 'round'
*/
strokeLinecap?: SVGAttributes<SVGCircleElement>['strokeLinecap'];
/**
* Class name for the circle
*/
className?: string;
textClassName?: string;
}
/**
* Component that show progression in the circle and the percentage in the middle
*/
function ProgressionCircle({
size = 40,
strokeWidth = 2,
strokeLinecap = 'butt',
className,
textClassName,
}: ProgressionCircleProps) {
const { progress, isReady } = useProgression();
if (!isReady) return null;
const radius = (size - strokeWidth) / 2;
const circumference = radius * 2 * Math.PI;
const strokeDasharray = circumference;
const strokeDashoffset = circumference - (progress / 100) * circumference;
return (
<div className={`relative inline-flex items-center justify-center ${className}`}>
<svg width={size} height={size} className="-rotate-90 transform">
{/* Background circle */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="currentColor"
strokeWidth={strokeWidth}
fill="none"
className="text-muted-foreground/20"
/>
{/* Progress circle */}
<circle
cx={size / 2}
cy={size / 2}
r={radius}
stroke="currentColor"
strokeWidth={strokeWidth}
fill="none"
strokeDasharray={strokeDasharray}
strokeDashoffset={strokeDashoffset}
strokeLinecap={strokeLinecap}
className="text-primary transition-all duration-300 ease-out"
/>
</svg>
{/* Percentage text in the center */}
<div className="absolute inset-0 flex items-center justify-center">
<span className={cn('text-foreground text-xs font-medium', textClassName)}>
{Math.round(progress)}%
</span>
</div>
</div>
);
}
export interface ProgressionBarProps {
/**
* Thickness of the bar
* @default 2
*/
thickness?: number;
/**
* Applied to the wrapper
*/
className?: string;
/**
* Applied to the percentage bar
*/
barClassName?: string;
/**
* Direction of the bar
* @default 'horizontal'
*/
direction?: 'horizontal' | 'vertical';
}
/**
* Component that show progression in the bar
*/
function ProgressionBar({
thickness = 2,
className,
direction = 'horizontal',
barClassName,
}: ProgressionBarProps) {
const { progress, isReady } = useProgression();
if (!isReady) return null;
return (
<div
className={cn(
direction === 'horizontal' ? 'w-full' : 'h-full',
'bg-muted-foreground/20 overflow-hidden rounded-full',
className
)}
style={direction === 'horizontal' ? { height: thickness } : { width: thickness }}
>
<div
className={cn(
direction === 'horizontal' ? 'h-full' : 'w-full',
'bg-primary rounded-full transition-all duration-300 ease-out',
barClassName
)}
style={direction === 'horizontal' ? { width: `${progress}%` } : { height: `${progress}%` }}
/>
</div>
);
}
export {
ProgressionRoot as Progression,
ProgressionBar,
ProgressionCircle,
ProgressionContent,
useProgression,
};
Update the import paths to match your project setup.
Usage
import { Progression, ProgressionBar, ProgressionCircle, ProgressionContent } from '@kit/ui/progression';
Basic Example
<Progression>
<ProgressionContent className="min-h-screen">
{/* Long content */}
</ProgressionContent>
<ProgressionCircle />
<ProgressionBar />
</Progression>
API Reference
Root
The root component that provides the progression context.
No props
Circle
Displays progress as a circular indicator with percentage text.
<ProgressionCircle
size={40}
strokeWidth={2}
strokeLinecap="round"
className="text-primary"
textClassName="text-xs font-medium"
/>
ProgressionCircleProps
Prop | Type | Default |
---|---|---|
size | number | 40 |
strokeWidth | number | 2 |
strokeLinecap | "butt" | "round" | "square"... | 'round' |
className | string | |
textClassName | string |
Bar
Displays progress as a horizontal or vertical bar.
<ProgressionBar
thickness={4}
className="w-full"
barClassName="bg-primary"
direction="horizontal"
/>
ProgressionBarProps
Prop | Type | Default |
---|---|---|
thickness | number | 2 |
className | string | |
barClassName | string | |
direction | "horizontal" | "vertical" | 'horizontal' |
Content
Wraps the content you want to track scroll progress for.
<ProgressionContent className="max-w-4xl mx-auto p-4">
{/* Your content */}
</ProgressionContent>
ProgressionContentProps
Prop | Type | Default |
---|---|---|
children* | React.ReactNode | |
asChild | boolean | false |
className | string | |
style | React.CSSProperties |
A phone input component that simplifies the phone input.
A compound component for generating QR codes with download functionality.
How is this guide?
Last updated on 10/17/2025