MC Progress
A component that displays progress of an operation.
Overview
The mc-progress component is a composable progress system for Micro Club UI.
It includes:
- Linear progress (
Track) with optional static and floating labels - Segmented progress (
Segments) for milestone-style feedback - Circular progress (
Circle) with optional value in the center - Stepper primitives (
Stepper,Step,StepLine) for multi-step flows
The component is optimized for library usage: each piece can be used alone or combined, and styling can be overridden with className and variant props.
Preview
import { McProgress, McProgressCircle, McProgressFloatingLabel, McProgressLabel, McProgressSegments, McProgressStep, McProgressStepLine, McProgressStepper, McProgressTrack,} from '@/components/ui/mc-progress';const progressValue = 68;const progressMax = 100;const stepItems = [1, 2, 3, 4];const currentStep = 3;export default function McProgressDemo() { return ( <div className="w-full max-w-md space-y-8"> <div className="space-y-3"> <p className="text-sm font-medium text-foreground">Linear Progress</p> <McProgress value={progressValue} max={progressMax} size="md"> <div className="flex items-center justify-between"> <span className="text-xs text-muted-foreground">Upload status</span> <McProgressLabel /> </div> <McProgressFloatingLabel /> <McProgressTrack /> </McProgress> </div> <div className="space-y-3"> <p className="text-sm font-medium text-foreground">Segmented Progress</p> <McProgress value={progressValue} max={progressMax} size="sm"> <McProgressSegments count={6} /> </McProgress> </div> <div className="flex items-start gap-8"> <div className="space-y-3"> <p className="text-sm font-medium text-foreground">Circle Progress</p> <McProgress value={progressValue} max={progressMax} size="lg"> <McProgressCircle showValue strokeWidth={16} /> </McProgress> </div> <div className="space-y-3"> <p className="text-sm font-medium text-foreground">Stepper Progress</p> <McProgressStepper orientation="vertical" size="md"> {stepItems.map((stepNumber) => { let status: 'completedBackground' | 'completedBorder' | 'active' | 'inactive' = 'inactive'; if (stepNumber < currentStep) { status = 'completedBackground'; } else if (stepNumber === currentStep) { status = 'active'; } return ( <div key={stepNumber} className="flex flex-col items-center"> <McProgressStep status={status} size="md" step={stepNumber} completed="icon" /> {stepNumber < stepItems.length && ( <McProgressStepLine active={stepNumber < currentStep} orientation="vertical" size="md" /> )} </div> ); })} </McProgressStepper> </div> </div> </div> );}Add to Your Project
Deploy the mc-progress directly to your components/ui directory:
Manual Setup
If you prefer manual control:
'use client';import * as React from 'react';import { cva, type VariantProps } from 'class-variance-authority';import { cn } from '@/lib/utils';import { Check } from 'lucide-react';interface ProgressContextValue { value: number; max: number; size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';}const ProgressContext = React.createContext<ProgressContextValue | null>(null);function useProgressContext() { const context = React.useContext(ProgressContext); if (!context) { throw new Error('McProgress sub-components must be used within <McProgress />'); } return context;}//root componentinterface McProgressProps extends React.HTMLAttributes<HTMLDivElement> { value: number; max?: number; size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'; children: React.ReactNode;}const McProgress = React.forwardRef<HTMLDivElement, McProgressProps>( ({ value, max = 100, size = 'md', className, children, ...props }, ref) => { const clampedValue = Math.min(max, Math.max(0, value)); const percentage = Math.round((clampedValue / max) * 100); return ( <ProgressContext.Provider value={{ value: percentage, max, size }}> <div ref={ref} role="progressbar" aria-valuenow={clampedValue} aria-valuemin={0} aria-valuemax={max} className={cn('w-full relative flex flex-col gap-1', className)} {...props} > {children} </div> </ProgressContext.Provider> ); });McProgress.displayName = 'McProgress';//bar componentconst progressBarVariants = cva('relative w-full bg-muted overflow-hidden border transition-all', { variants: { size: { xs: 'h-1 rounded-xs', sm: 'h-1.5 rounded-xs', md: 'h-2.5 rounded-sm', lg: 'h-5.5 rounded-lg', xl: 'h-7 rounded-xl', }, }, defaultVariants: { size: 'md' },});const progressLabelVariants = cva('font-medium whitespace-nowrap', { variants: { size: { xs: 'text-[10px]', sm: 'text-xs', md: 'text-base', lg: 'text-lg', xl: 'text-xl', }, }, defaultVariants: { size: 'md' },});interface ProgressTrackProps extends React.HTMLAttributes<HTMLDivElement> { barClassName?: string;}const McProgressTrack = React.forwardRef<HTMLDivElement, ProgressTrackProps>( ({ className, barClassName, ...props }, ref) => { const { value, size } = useProgressContext(); return ( <div ref={ref} className={cn(progressBarVariants({ size }), className)} {...props}> <div className={cn('h-full bg-primary transition-all duration-300 ease-out', barClassName)} style={{ width: `${value}%` }} /> </div> ); });McProgressTrack.displayName = 'McProgressTrack';//segmented bar componentinterface ProgressSegmentsProps extends React.HTMLAttributes<HTMLDivElement> { count?: number;}const McProgressSegments = React.forwardRef<HTMLDivElement, ProgressSegmentsProps>( ({ count = 4, className, ...props }, ref) => { const { value, size } = useProgressContext(); return ( <div ref={ref} className={cn('flex gap-1 w-full', className)} {...props}> {Array.from({ length: count }).map((_, i) => { const stepValue = (100 / count) * (i + 1); const isFilled = value >= stepValue; return ( <div key={i} className={cn( progressBarVariants({ size }), 'flex-1', isFilled ? 'bg-primary' : 'bg-muted' )} /> ); })} </div> ); });McProgressSegments.displayName = 'McProgressSegments';//dynamic floating label componentinterface ProgressFloatingLabelProps extends React.HTMLAttributes<HTMLSpanElement> { formatter?: (value: number) => React.ReactNode;}const McProgressFloatingLabel = React.forwardRef<HTMLSpanElement, ProgressFloatingLabelProps>( ({ formatter, className, ...props }, ref) => { const { value, size } = useProgressContext(); return ( <span ref={ref} className={cn( 'absolute bottom-full font-semibold text-primary transition-all -translate-x-1/2', progressLabelVariants({ size }), className )} style={{ left: `${value}%` }} {...props} > {formatter ? formatter(value) : `${value}%`} </span> ); });McProgressFloatingLabel.displayName = 'McProgressFloatingLabel';//static label componentinterface ProgressLabelProps extends React.HTMLAttributes<HTMLSpanElement> { formatter?: (value: number) => React.ReactNode;}const McProgressLabel = React.forwardRef<HTMLSpanElement, ProgressLabelProps>( ({ formatter, className, ...props }, ref) => { const { value, size } = useProgressContext(); return ( <span ref={ref} className={cn(progressLabelVariants({ size }), 'text-muted-foreground', className)} {...props} > {formatter ? formatter(value) : `${value}%`} </span> ); });McProgressLabel.displayName = 'McProgressLabel';//circle componentinterface ProgressCircleProps extends React.SVGAttributes<SVGSVGElement> { showValue?: boolean; strokeWidth?: number;}const circleSizes = { xs: { size: 44, stroke: 4, maxStroke: 8, fontSize: 'text-[10px]' }, sm: { size: 60, stroke: 6, maxStroke: 12, fontSize: 'text-sm' }, md: { size: 84, stroke: 8, maxStroke: 20, fontSize: 'text-base' }, lg: { size: 106, stroke: 12, maxStroke: 24, fontSize: 'text-lg' }, xl: { size: 132, stroke: 14, maxStroke: 28, fontSize: 'text-xl' },};const McProgressCircle = React.forwardRef<SVGSVGElement, ProgressCircleProps>( ({ className, showValue = false, strokeWidth, ...props }, ref) => { const { value, size = 'md' } = useProgressContext(); const config = circleSizes[size]; const sw = Math.min(strokeWidth ?? config.stroke, config.maxStroke); const radius = (config.size - sw) / 2; const circumference = 2 * Math.PI * radius; const strokeDashoffset = circumference - (value / 100) * circumference; return ( <div className="relative inline-flex items-center justify-center"> <svg ref={ref} width={config.size} height={config.size} className={cn('transform -rotate-90', className)} {...props} > {/* inactive circle */} <circle cx={config.size / 2} cy={config.size / 2} r={radius} className="stroke-muted fill-none" strokeWidth={sw} /> {/* active circle */} <circle cx={config.size / 2} cy={config.size / 2} r={radius} className="stroke-primary fill-none transition-all duration-300 ease-out" strokeWidth={sw} strokeDasharray={circumference} strokeDashoffset={strokeDashoffset} strokeLinecap="round" /> </svg> {showValue && ( <span className={`absolute ${config.fontSize} font-semibold text-foreground`}> {value}% </span> )} </div> ); });McProgressCircle.displayName = 'McProgressCircle';//stepper horizontal/vertical componentinterface StepperProps extends React.HTMLAttributes<HTMLDivElement> { orientation?: 'horizontal' | 'vertical'; size?: 'sm' | 'md' | 'lg';}const McProgressStepper = React.forwardRef<HTMLDivElement, StepperProps>( ({ orientation = 'horizontal', size = 'md', className, children, ...props }, ref) => { const spacingClass = size === 'lg' ? 'gap-4' : size === 'md' ? 'gap-2' : 'gap-1'; return ( <div ref={ref} className={cn( 'flex w-full items-center', orientation === 'vertical' ? cn('flex-col items-center', spacingClass) : cn('flex-row items-center', spacingClass), className )} {...props} > {children} </div> ); });McProgressStepper.displayName = 'McProgressStepper';const stepVariants = cva( 'relative flex items-center justify-center rounded-full font-medium transition-colors border-2 shrink-0', { variants: { status: { completedBackground: 'bg-primary text-primary-foreground', completedBorder: 'bg-white border-primary ', active: 'border-primary text-primary font-bold bg-background', inactive: 'border-muted text-muted-foreground bg-muted/30', outlinedCompleted: 'border-primary text-primary bg-background font-semibold', }, size: { sm: 'w-8 h-8 text-sm ', md: 'w-12 h-12 text-base', lg: 'w-14 h-14 text-xl ', }, }, compoundVariants: [ { status: 'completedBorder', size: 'sm', className: 'border-6', }, { status: 'completedBorder', size: 'md', className: 'border-8', }, { status: 'completedBorder', size: 'lg', className: 'border-12', }, ], defaultVariants: { status: 'inactive', size: 'md', }, });interface StepProps extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof stepVariants> { step?: number; completed?: 'number' | 'icon'; icon?: React.ReactNode;}//step componentconst McProgressStep = React.forwardRef<HTMLDivElement, StepProps>( ({ status, size, step, completed = 'icon', icon, children, className, ...props }, ref) => { const isCompleted = status === 'completedBackground' || status === 'completedBorder'; const checkSizeClass = size === 'lg' ? 'w-6 h-6' : size === 'md' ? 'w-5 h-5' : 'w-4 h-4'; const renderContent = () => { if (isCompleted && completed === 'icon') { if (icon) { return icon; } return status === 'completedBackground' ? ( <Check className={cn(checkSizeClass, 'text-primary-foreground font-bold')} /> ) : ( <Check className={cn(checkSizeClass, 'text-primary font-bold')} /> ); } if (isCompleted && completed === 'number') { return status === 'completedBackground' ? ( <span className="text-primary-foreground font-bold">{step}</span> ) : ( <span className="text-primary font-bold">{step}</span> ); } return children || step; }; return ( <div ref={ref} className={cn(stepVariants({ status, size }), className)} {...props}> {renderContent()} </div> ); });McProgressStep.displayName = 'McProgressStep';interface StepLineProps extends React.HTMLAttributes<HTMLDivElement> { active?: boolean; orientation?: 'horizontal' | 'vertical'; size?: 'sm' | 'md' | 'lg';}//step line componentconst stepLineVariants = cva('transition-colors duration-300', { variants: { size: { sm: 'rounded-sm', md: 'rounded-md', lg: 'rounded-lg', }, }, defaultVariants: { size: 'md', },});const McProgressStepLine = React.forwardRef<HTMLDivElement, StepLineProps>( ({ active = false, orientation = 'horizontal', size = 'md', className, ...props }, ref) => { const horizontalSizeClass = size === 'lg' ? 'h-1 min-w-16 flex-1' : size === 'md' ? 'h-0.5 min-w-12 flex-1' : 'h-px min-w-8 flex-1'; const verticalSizeClass = size === 'lg' ? 'w-1 h-16 self-center' : size === 'md' ? 'w-0.5 h-12 self-center' : 'w-px h-8 self-center'; return ( <div ref={ref} className={cn( stepLineVariants({ size }), orientation === 'horizontal' ? horizontalSizeClass : verticalSizeClass, active ? 'bg-primary' : 'bg-muted', className )} {...props} /> ); });McProgressStepLine.displayName = 'McProgressStepLine';export { McProgress, McProgressTrack, McProgressSegments, McProgressFloatingLabel, McProgressLabel, McProgressCircle, McProgressStepper, McProgressStep, McProgressStepLine,};Ensure your @/lib/utils (or equivalent classname helper) is correctly configured.
Usage
import {
McProgress,
McProgressLabel,
McProgressFloatingLabel,
McProgressTrack,
} from '@/components/ui/mc-progress';
export default function Example() {
return (
<McProgress value={62} max={100} size="md">
<div className="flex items-center justify-between">
<span className="text-xs text-muted-foreground">Upload status</span>
<McProgressLabel />
</div>
<McProgressFloatingLabel />
<McProgressTrack />
</McProgress>
);
}Examples
Linear Progress with Header and Floating Label
<McProgress value={72} max={100} size="lg">
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">Deployment</span>
<McProgressLabel formatter={(value) => `${value}% complete`} />
</div>
<McProgressFloatingLabel />
<McProgressTrack />
</McProgress>Segmented Progress
<McProgress value={40} max={100} size="sm">
<McProgressSegments count={5} />
</McProgress>Circular Progress
<McProgress value={68} max={100} size="md">
<McProgressCircle showValue strokeWidth={10} />
</McProgress>Circular Progress Sizes and Stroke Limits
McProgressCircle applies a max stroke width per size:
xs: max8sm: max12md: max20lg: max24xl: max28
If a larger value is passed, it is clamped automatically.
Horizontal Stepper
<McProgressStepper orientation="horizontal" size="md">
<McProgressStep status="completedBackground" step={1} completed="icon" />
<McProgressStepLine active orientation="horizontal" size="md" />
<McProgressStep status="completedBorder" step={2} completed="number" />
<McProgressStepLine active orientation="horizontal" size="md" />
<McProgressStep status="active" step={3} />
<McProgressStepLine orientation="horizontal" size="md" />
<McProgressStep status="inactive" step={4} />
</McProgressStepper>Vertical Stepper
<McProgressStepper orientation="vertical" size="lg">
<div className="flex flex-col items-center">
<McProgressStep status="completedBackground" step={1} completed="icon" />
<McProgressStepLine active orientation="vertical" size="lg" />
</div>
<div className="flex flex-col items-center">
<McProgressStep status="active" step={2} />
<McProgressStepLine orientation="vertical" size="lg" />
</div>
<McProgressStep status="inactive" step={3} />
</McProgressStepper>Custom Completed Icon
import { Star } from 'lucide-react';
<McProgressStep
status="completedBackground"
size="md"
completed="icon"
icon={<Star className="h-5 w-5 text-primary-foreground" />}
/>;API Reference
McProgress (Root)
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | - | Current progress value |
max | number | 100 | Maximum progress value |
size | "xs" | "sm" | "md" | "lg" | "xl" | "md" | Global size used by track, labels, and circle |
className | string | undefined | Extra classes for the root wrapper |
children | React.ReactNode | - | Composed progress children |
Also supports standard HTMLDivElement props.
McProgressTrack
| Prop | Type | Default | Description |
|---|---|---|---|
barClassName | string | undefined | Extra classes for the filled bar element |
className | string | undefined | Extra classes for the track container |
Also supports standard HTMLDivElement props.
McProgressSegments
| Prop | Type | Default | Description |
|---|---|---|---|
count | number | 4 | Number of segments |
className | string | undefined | Extra classes for segment wrapper |
Also supports standard HTMLDivElement props.
McProgressLabel
| Prop | Type | Default | Description |
|---|---|---|---|
formatter | (value: number) => React.ReactNode | undefined | Custom renderer for label text |
className | string | undefined | Extra classes for the label |
Also supports standard HTMLSpanElement props.
McProgressFloatingLabel
| Prop | Type | Default | Description |
|---|---|---|---|
formatter | (value: number) => React.ReactNode | undefined | Custom renderer for floating label text |
className | string | undefined | Extra classes for floating label |
Also supports standard HTMLSpanElement props.
McProgressCircle
| Prop | Type | Default | Description |
|---|---|---|---|
showValue | boolean | false | Display the numeric percentage at center |
strokeWidth | number | per-size default | Stroke width for the active/inactive circles (clamped by size) |
className | string | undefined | Extra classes for the SVG element |
Also supports standard SVGSVGElement props.
McProgressStepper
| Prop | Type | Default | Description |
|---|---|---|---|
orientation | "horizontal" | "vertical" | "horizontal" | Stepper axis direction |
size | "sm" | "md" | "lg" | "md" | Gap between steps. Set size on each step and line too |
className | string | undefined | Extra classes for the stepper wrapper |
Also supports standard HTMLDivElement props.
McProgressStep
| Prop | Type | Default | Description |
|---|---|---|---|
status | "completedBackground" | "completedBorder" | "active" | "inactive" | "outlinedCompleted" | "inactive" | Visual state of the step |
size | "sm" | "md" | "lg" | "md" | Step circle size |
step | number | undefined | Number displayed when content is not overridden |
completed | "number" | "icon" | "icon" | Display mode for completed states |
icon | React.ReactNode | undefined | Custom completed icon (used when completed="icon") |
className | string | undefined | Extra classes for the step |
Also supports standard HTMLDivElement props.
McProgressStepLine
| Prop | Type | Default | Description |
|---|---|---|---|
active | boolean | false | Applies active color for completed connections |
orientation | "horizontal" | "vertical" | "horizontal" | Connector direction |
size | "sm" | "md" | "lg" | "md" | Connector thickness, radius, and length |
className | string | undefined | Extra classes for the connector line |
Also supports standard HTMLDivElement props.