Appearance
Circular Progress Component Code
Dependencies
This component requires:
- Vue 3 with Composition API
Full Component Code
vue
<script setup>
import { computed } from "vue";
const props = defineProps({
variant: {
type: String,
default: "default",
},
indeterminate: {
type: Boolean,
default: false,
},
modelValue: {
type: Number,
default: 0,
},
rotate: {
type: Number,
default: 0,
},
size: {
type: Number,
default: 100,
},
width: {
type: Number,
default: 4,
},
showValue: {
type: Boolean,
default: false,
},
round: {
type: Boolean,
default: false,
},
});
const radius = computed(() => 50 - props.width / 2);
const circumference = computed(() => 2 * Math.PI * radius.value);
const strokeDashoffset = computed(() => {
return circumference.value * (1 - props.modelValue / 100);
});
const valueFontSize = computed(() => `${Math.min(props.size * 0.25, 24)}px`);
</script>
<template>
<div
class="circular-progress"
:class="[variant]"
:style="{
width: `${size}px`,
height: `${size}px`,
'--circumference': circumference + 'px',
}"
>
<svg
class="circular-progress-svg"
:width="size"
:height="size"
viewBox="0 0 100 100"
>
<circle
class="circular-progress-background"
cx="50"
cy="50"
fill="none"
:r="radius"
:stroke-width="width"
stroke-linecap="butt"
/>
<g
:transform="`rotate(${rotate - 90}, 50, 50)`"
:class="{ 'circular-progress-indeterminate-group': indeterminate }"
>
<circle
class="circular-progress-circle"
:class="{ 'circular-progress-indeterminate': indeterminate }"
cx="50"
cy="50"
fill="none"
:r="radius"
:stroke-width="width"
:stroke-dasharray="`${circumference} ${circumference}`"
:stroke-dashoffset="strokeDashoffset"
:stroke-linecap="round ? 'round' : 'butt'"
/>
</g>
</svg>
<div
v-if="showValue && !indeterminate"
class="circular-progress-value"
:style="{ fontSize: valueFontSize }"
>
{{ Math.round(modelValue) }}%
</div>
</div>
</template>
<style scoped lang="scss">
.circular-progress {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
.circular-progress-svg {
display: block;
}
.circular-progress-background {
fill: none;
}
.circular-progress-indeterminate-group {
animation: rotate-indeterminate 0.75s linear infinite;
transform-origin: 50% 50%;
.circular-progress-circle {
fill: none;
transition: stroke-dashoffset 0.3s ease;
&.circular-progress-indeterminate {
stroke-linecap: round;
animation: dash-animation 1.5s linear infinite;
}
}
}
.circular-progress-value {
position: absolute;
font-weight: 500;
line-height: 1;
}
// Variants
&.default {
.circular-progress-background {
stroke: #ddd;
}
.circular-progress-circle {
stroke: #000;
}
.circular-progress-value {
color: #000;
}
}
&.success {
.circular-progress-background {
stroke: #ddd;
}
.circular-progress-circle {
stroke: #22c55e;
}
.circular-progress-value {
color: #22c55e;
}
}
}
@keyframes dash-animation {
0% {
stroke-dasharray: calc(var(--circumference) * 0.95), var(--circumference);
stroke-dashoffset: 0;
}
30% {
stroke-dasharray: calc(var(--circumference) * 0.01), var(--circumference);
stroke-dashoffset: calc(var(--circumference) * -1);
}
50% {
stroke-dasharray: calc(var(--circumference) * 0.01), var(--circumference);
stroke-dashoffset: calc(var(--circumference) * -1);
}
70% {
stroke-dasharray: calc(var(--circumference) * 0.95), var(--circumference);
stroke-dashoffset: calc(var(--circumference) * -2);
}
100% {
stroke-dasharray: calc(var(--circumference) * 0.95), var(--circumference);
stroke-dashoffset: calc(var(--circumference) * -2);
}
}
@keyframes rotate-indeterminate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>