42

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

Loading...
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:

npx mcoli-ui@latest add mc-progress
pnpm dlx mcoli-ui@latest add mc-progress
yarn dlx mcoli-ui@latest add mc-progress
bunx mcoli-ui@latest add mc-progress

Manual Setup

If you prefer manual control:

Install the core dependencies
npm install lucide-react class-variance-authority
pnpm add lucide-react class-variance-authority
yarn add lucide-react class-variance-authority
bun add lucide-react class-variance-authority
Copy the source code
'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,};
Adjust import paths

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: max 8
  • sm: max 12
  • md: max 20
  • lg: max 24
  • xl: max 28

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)

PropTypeDefaultDescription
valuenumber-Current progress value
maxnumber100Maximum progress value
size"xs" | "sm" | "md" | "lg" | "xl""md"Global size used by track, labels, and circle
classNamestringundefinedExtra classes for the root wrapper
childrenReact.ReactNode-Composed progress children

Also supports standard HTMLDivElement props.

McProgressTrack

PropTypeDefaultDescription
barClassNamestringundefinedExtra classes for the filled bar element
classNamestringundefinedExtra classes for the track container

Also supports standard HTMLDivElement props.

McProgressSegments

PropTypeDefaultDescription
countnumber4Number of segments
classNamestringundefinedExtra classes for segment wrapper

Also supports standard HTMLDivElement props.

McProgressLabel

PropTypeDefaultDescription
formatter(value: number) => React.ReactNodeundefinedCustom renderer for label text
classNamestringundefinedExtra classes for the label

Also supports standard HTMLSpanElement props.

McProgressFloatingLabel

PropTypeDefaultDescription
formatter(value: number) => React.ReactNodeundefinedCustom renderer for floating label text
classNamestringundefinedExtra classes for floating label

Also supports standard HTMLSpanElement props.

McProgressCircle

PropTypeDefaultDescription
showValuebooleanfalseDisplay the numeric percentage at center
strokeWidthnumberper-size defaultStroke width for the active/inactive circles (clamped by size)
classNamestringundefinedExtra classes for the SVG element

Also supports standard SVGSVGElement props.

McProgressStepper

PropTypeDefaultDescription
orientation"horizontal" | "vertical""horizontal"Stepper axis direction
size"sm" | "md" | "lg""md"Gap between steps. Set size on each step and line too
classNamestringundefinedExtra classes for the stepper wrapper

Also supports standard HTMLDivElement props.

McProgressStep

PropTypeDefaultDescription
status"completedBackground" | "completedBorder" | "active" | "inactive" | "outlinedCompleted""inactive"Visual state of the step
size"sm" | "md" | "lg""md"Step circle size
stepnumberundefinedNumber displayed when content is not overridden
completed"number" | "icon""icon"Display mode for completed states
iconReact.ReactNodeundefinedCustom completed icon (used when completed="icon")
classNamestringundefinedExtra classes for the step

Also supports standard HTMLDivElement props.

McProgressStepLine

PropTypeDefaultDescription
activebooleanfalseApplies active color for completed connections
orientation"horizontal" | "vertical""horizontal"Connector direction
size"sm" | "md" | "lg""md"Connector thickness, radius, and length
classNamestringundefinedExtra classes for the connector line

Also supports standard HTMLDivElement props.

On this page