Circular Progress
circular-progress ui Radial/circular progress indicator. Supports value (0-100), size presets or custom pixel size, stroke thickness, custom arc and track colors, indeterminate spinning mode, a label slot for center content, and a show-value prop that renders the percentage in the center.
Also available for Vue ->Installation
$ pnpm dlx shadcn@latest add https://uipkge.dev/r/react/circular-progress.json $ npx shadcn@latest add https://uipkge.dev/r/react/circular-progress.json $ yarn dlx shadcn@latest add https://uipkge.dev/r/react/circular-progress.json $ bunx shadcn@latest add https://uipkge.dev/r/react/circular-progress.json Named registry:
npx shadcn@latest add @uipkge-react/circular-progress Installs to: components/ui/circular-progress/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
value Progress value 0-100. Ignored when indeterminate is true. | number | — | optional |
size Diameter in pixels. | 'sm' | 'default' | 'lg' | number | — | optional |
thickness Stroke thickness in pixels. | number | — | optional |
color Progress arc color. Defaults to primary. | string | — | optional |
trackColor Track (background ring) color. | string | — | optional |
indeterminate Indeterminate spinning mode. | boolean | — | optional |
showValue Show the numeric value in the center. | boolean | — | optional |
suffix Suffix appended to the value (e.g. '%'). | string | — | optional |
ariaLabel Accessible label. | string | — | optional |
npm dependencies
Files installed (3)
-
components/ui/circular-progress/CircularProgress.tsx 4.7 kB
import * as React from 'react' import { cn } from '@/lib/utils' import { circularProgressVariants } from './circular-progress.variants' export interface CircularProgressProps extends React.HTMLAttributes<HTMLDivElement> { /** Progress value 0-100. Ignored when indeterminate is true. */ value?: number /** Diameter in pixels. */ size?: 'sm' | 'default' | 'lg' | number /** Stroke thickness in pixels. */ thickness?: number /** Progress arc color. Defaults to primary. */ color?: string /** Track (background ring) color. */ trackColor?: string /** Indeterminate spinning mode. */ indeterminate?: boolean /** Show the numeric value in the center. */ showValue?: boolean /** Suffix appended to the value (e.g. '%'). */ suffix?: string /** Accessible label. */ ariaLabel?: string } const sizePxMap = { sm: 40, default: 56, lg: 80, } as const const CircularProgress = React.forwardRef<HTMLDivElement, CircularProgressProps>( ( { className, value = 0, size = 'default', thickness = 8, color, trackColor, indeterminate = false, showValue = false, suffix = '%', ariaLabel = 'Progress', children, ...props }, ref, ) => { const sizePx = React.useMemo(() => { if (typeof size === 'number') return size return sizePxMap[size] ?? 56 }, [size]) const normalizedValue = React.useMemo(() => Math.min(100, Math.max(0, value)), [value]) const radius = React.useMemo(() => (sizePx - thickness) / 2, [sizePx, thickness]) const circumference = React.useMemo(() => 2 * Math.PI * radius, [radius]) const strokeDashoffset = React.useMemo(() => { if (indeterminate) return circumference * 0.25 return circumference * (1 - normalizedValue / 100) }, [indeterminate, circumference, normalizedValue]) const resolvedColor = React.useMemo(() => color || 'var(--primary)', [color]) const resolvedTrackColor = React.useMemo(() => trackColor || 'var(--muted)', [trackColor]) const viewBox = React.useMemo(() => `0 0 ${sizePx} ${sizePx}`, [sizePx]) const center = React.useMemo(() => sizePx / 2, [sizePx]) const fontSize = React.useMemo(() => { const s = sizePx if (s <= 40) return 'text-xs' if (s <= 56) return 'text-sm' return 'text-base' }, [sizePx]) return ( <div ref={ref} data-uipkge="" data-slot="circular-progress" data-size={typeof size === 'string' ? size : 'custom'} data-indeterminate={indeterminate ? 'true' : 'false'} className={cn(circularProgressVariants(), className)} style={{ width: `${sizePx}px`, height: `${sizePx}px` }} role="progressbar" aria-valuemin={0} aria-valuemax={100} aria-valuenow={indeterminate ? undefined : normalizedValue} aria-label={ariaLabel} {...props} > <style>{` @media (prefers-reduced-motion: no-preference) { .animate-spin-circular { animation: spin-circular 1.4s linear infinite; } } @keyframes spin-circular { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } `}</style> <svg width={sizePx} height={sizePx} viewBox={viewBox} className="block"> {/* Track */} <circle cx={center} cy={center} r={radius} fill="none" stroke={resolvedTrackColor} strokeWidth={thickness} /> {/* Progress arc */} <g transform={indeterminate ? undefined : `rotate(-90 ${center} ${center})`} className={indeterminate ? 'animate-spin-circular' : ''} style={indeterminate ? { transformBox: 'fill-box', transformOrigin: 'center' } : undefined} > <circle cx={center} cy={center} r={radius} fill="none" stroke={resolvedColor} strokeWidth={thickness} strokeLinecap="round" strokeDasharray={circumference} strokeDashoffset={strokeDashoffset} className={!indeterminate ? 'transition-[stroke-dashoffset] duration-500 ease-out' : ''} /> </g> </svg> {showValue || children ? ( <div className="absolute inset-0 flex items-center justify-center"> {children ? ( children ) : ( <span className={cn('text-foreground font-medium tabular-nums', fontSize)}> {Math.round(normalizedValue)} {suffix} </span> )} </div> ) : null} </div> ) }, ) CircularProgress.displayName = 'CircularProgress' export { CircularProgress } -
components/ui/circular-progress/circular-progress.variants.ts 0.6 kB
import type { VariantProps } from 'class-variance-authority' import { cva } from 'class-variance-authority' /** * Variant definitions live in their own file (rather than the package * `index.ts`) so `CircularProgress.tsx` can `import { circularProgressVariants } from * './circular-progress.variants'` without creating a circular dependency through * the index. See card.variants.ts for the same pattern + the SSR symptom that * motivated the split. */ export const circularProgressVariants = cva('relative inline-flex items-center justify-center') export type CircularProgressVariants = VariantProps<typeof circularProgressVariants> -
components/ui/circular-progress/index.ts 0.2 kB
export { CircularProgress, type CircularProgressProps } from './CircularProgress' export { circularProgressVariants, type CircularProgressVariants } from './circular-progress.variants'
Raw manifest: https://uipkge.dev/r/react/circular-progress.json