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 React ->Installation
$ pnpm dlx shadcn-vue@latest add https://uipkge.dev/r/vue/circular-progress.json $ npx shadcn-vue@latest add https://uipkge.dev/r/vue/circular-progress.json $ yarn dlx shadcn-vue@latest add https://uipkge.dev/r/vue/circular-progress.json $ bunx shadcn-vue@latest add https://uipkge.dev/r/vue/circular-progress.json Named registry:
npx shadcn-vue@latest add @uipkge/circular-progress Installs to: app/components/ui/circular-progress/ Examples
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
class | HTMLAttributes['class'] | — | optional |
value Progress value 0-100. Ignored when indeterminate is true. | number | 0 | optional |
size Diameter in pixels. | 'sm' | 'default' | 'lg' | number | 'default' | optional |
thickness Stroke thickness in pixels. | number | 8 | optional |
color Progress arc color. Defaults to primary. | string | — | optional |
trackColor Track (background ring) color. | string | — | optional |
indeterminate Indeterminate spinning mode. | boolean | false | optional |
showValue Show the numeric value in the center. | boolean | false | optional |
suffix Suffix appended to the value (e.g. '%'). | string | '%' | optional |
ariaLabel Accessible label. | string | 'Progress' | optional |
npm dependencies
Files installed (3)
-
app/components/ui/circular-progress/CircularProgress.vue 4.2 kB
<script setup lang="ts"> import type { HTMLAttributes } from 'vue' import { computed } from 'vue' import { cn } from '@/lib/utils' import { circularProgressVariants } from './circular-progress.variants' // Inlined union: SFC compiler can't extract runtime props from // `CircularProgressVariants['size']`. const props = withDefaults( defineProps<{ class?: HTMLAttributes['class'] /** 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 }>(), { value: 0, size: 'default', thickness: 8, indeterminate: false, showValue: false, suffix: '%', ariaLabel: 'Progress', }, ) const sizePx = computed(() => { if (typeof props.size === 'number') return props.size switch (props.size) { case 'sm': return 40 case 'lg': return 80 default: return 56 } }) const normalizedValue = computed(() => Math.min(100, Math.max(0, props.value))) const radius = computed(() => (sizePx.value - props.thickness) / 2) const circumference = computed(() => 2 * Math.PI * radius.value) const strokeDashoffset = computed(() => { if (props.indeterminate) return circumference.value * 0.25 return circumference.value * (1 - normalizedValue.value / 100) }) const resolvedColor = computed(() => props.color || 'var(--primary)') const resolvedTrackColor = computed(() => props.trackColor || 'var(--muted)') const viewBox = computed(() => `0 0 ${sizePx.value} ${sizePx.value}`) const center = computed(() => sizePx.value / 2) const fontSize = computed(() => { const s = sizePx.value if (s <= 40) return 'text-xs' if (s <= 56) return 'text-sm' return 'text-base' }) </script> <template> <div data-uipkge data-slot="circular-progress" :data-size="typeof size === 'string' ? size : 'custom'" :data-indeterminate="indeterminate ? 'true' : 'false'" :class="cn(circularProgressVariants(), props.class)" :style="{ width: `${sizePx}px`, height: `${sizePx}px` }" role="progressbar" :aria-valuemin="0" :aria-valuemax="100" :aria-valuenow="indeterminate ? undefined : normalizedValue" :aria-label="ariaLabel" > <svg :width="sizePx" :height="sizePx" :viewBox="viewBox" class="block"> <!-- Track --> <circle :cx="center" :cy="center" :r="radius" fill="none" :stroke="resolvedTrackColor" :stroke-width="thickness" /> <!-- Progress arc --> <g :transform="indeterminate ? undefined : `rotate(-90 ${center} ${center})`" :class="indeterminate ? 'animate-spin-circular' : ''" :style="indeterminate ? { transformBox: 'fill-box', transformOrigin: 'center' } : undefined" > <circle :cx="center" :cy="center" :r="radius" fill="none" :stroke="resolvedColor" :stroke-width="thickness" stroke-linecap="round" :stroke-dasharray="circumference" :stroke-dashoffset="strokeDashoffset" :class="!indeterminate ? 'transition-[stroke-dashoffset] duration-500 ease-out' : ''" /> </g> </svg> <div v-if="showValue || $slots.default" class="absolute inset-0 flex items-center justify-center"> <slot :value="normalizedValue"> <span v-if="showValue" :class="cn('text-foreground font-medium tabular-nums', fontSize)"> {{ Math.round(normalizedValue) }}{{ suffix }} </span> </slot> </div> </div> </template> <style scoped> @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> -
app/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.vue` 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> -
app/components/ui/circular-progress/index.ts 0.3 kB
export { default as CircularProgress } from './CircularProgress.vue' // Re-export variant API from the sibling file (kept separate to avoid the // CircularProgress.vue <-> index.ts circular import that broke dev SSR for Card). export { circularProgressVariants, type CircularProgressVariants } from './circular-progress.variants'
Raw manifest: https://uipkge.dev/r/vue/circular-progress.json