Skip to content

circle-progress 2.2.0

To take full control over the appearance of the circle progress, you can use the circle-progress slot. This scoped slot allows you to replace the default circle progress with any SVG or HTML content you want. The slot exposes all the (parsed) props passed to the component and also all necessary internal data to create default circle progress.

Experimental feature

This feature lets you sneak into the internals of the plugin and gives you a lot of power to customize the circle. It's challenging to provide good defaults for all possible use cases as you can place any content inside the slot. So you might encounter some strange inconsistencies in conjunction with other props. Basically, the slot just drops its content instead of the default circle progress and doesn't care much about how it fits into the rest of the component. Now it's up to you to make it look good.

Usage 📜

vue
<ve-progress :progress="50">
  <template #circle-progress="{ attrs }">
    <circle
      :r="attrs.radius"
      :cx="attrs.position"
      :cy="attrs.position"
      fill="transparent"
      :stroke="attrs.color"
      :stroke-width="attrs.thickness"
      :stroke-linecap="attrs.line"
      :stroke-dasharray="attrs.circumference"
      :style="attrs.styles"
    >
    </circle>
  </template>
</ve-progress>

The attrs object contains all the necessary data to create circle progress. Here are a few important properties:

PropertyNotes
radiusradius of the circle calculated depending on other props
postioncalculated position to place the circle correctly inside the SVG
circumferencethe circumference (SVG path length) of the progress circle
stylesall the necessary styles to add smooth animations
strokeDashOffsetoffset calculated based on the progress value, applicable only for circle elements like <circle> or <ellipse>
calculateProgressOffsethelper function accepting element path length to help you to calculate progress offset for SVG elements other than circle
class animationClassClasses to apply animations

Inspect the attrs object to see all the available properties and their values. After the migration to TypeScript, the plugin will provide you with a better type definitions. The following examples will explain how and when to use these properties.

Slot recipe

  1. Draw your SVG element inside the slot. If the element is not an ellipse-like element, you have to position it correctly by yourself.
  2. Set the stroke-dasharray attribute to the path length of the SVG element. For the ellipse-like elements, you can use the attrs.circumference value.
  3. Set the stroke-dashoffset attribute to the attrs.strokeDashOffset value for the ellipse-like elements. This value is calculated depending on the progress value. For other SVG elements, you can use the attrs.calculateProgressOffset(pathLength) function to calculate the progress offset.

Recreating the circle

First, we will recreate the default circle progress. I assume often you will use the slot to apply some custom styles or classes to the default circle progress. The plugin is optimized to show circular progress bars, so all the exposed properties in the attrs object are ready to be used to create a circle. The following example shows the exact SVG element that the plugin uses under the hood to create the default circle progress.

vue
<ve-progress :progress="50">
  <template #circle-progress="{ attrs }">
    <circle
      :r="attrs.radius"
      :cx="attrs.position"
      :cy="attrs.position"
      fill="transparent"
      :stroke="attrs.color"
      :stroke-width="attrs.thickness"
      :stroke-linecap="attrs.line"
      :stroke-dasharray="attrs.circumference"
      :style="attrs.styles"
    >
    </circle>
  </template>
</ve-progress>

The same also can be done with the <ellipse> SVG element.

vue
<ve-progress :progress="50">
  <template #circle-progress="{ attrs }">
    <ellipse
      :cx="attrs.position"
      :cy="attrs.position"
      :rx="attrs.radius"
      :ry="attrs.radius"
      :stroke-dasharray="attrs.circumference"
      :stroke-dashoffset="attrs.strokeDashOffset"
      :stroke-width="attrs.thickness"
      :stroke="attrs.color"
      :stroke-linecap="attrs.line"
      :class="[attrs.class, attrs.animationClass]"
      fill="transparent"
      :style="attrs.styles"
    />
  </template>
</ve-progress>

Beyond the circle

You can put anything inside the slot. There is no guarantee that the exposed properties will fit your SVG element. For example, the strokeDashOffset that "cuts" the circle line depending on the progress value is calculated depending on the circle circumference and therefore wouldn't work correctly for other SVG elements.

The function attrs.calculateProgressOffset helps you to calculate the progress offset for other SVG elements, with one caveat: you have to pass the path length of the element to the function.

Getting the path length

You can use svgElement.getTotalLength() to get the path length of the SVG element. In the future, this experimental feature might be improved to overcome this inconvenience.

Let's start with a simple example. Here we are drawing a polygon that still can show progress.

vue
<ve-progress empty-thickness="0" color="#1ABC9C" :progress="50">
  <template #circle-progress="{ attrs }">
    <polygon
      ref="polygon"
      :stroke-dashoffset="attrs.calculateProgressOffset(pathLength)"
      :stroke-dasharray="pathLength"
      points="10,10 190,100 10,190"
      style="fill: #7b68ee"
      :stroke-width="attrs.thickness"
      :stroke-linecap="attrs.line"
      :stroke="attrs.color"
      :style="attrs.styles"
    />
  </template>
</ve-progress>
<script setup lang="ts">
import { ref, useTemplateRef, watch } from "vue";

const polygon = useTemplateRef("polygon");
const pathLength = ref(0);

watch(polygon, () => {
  pathLength.value = polygon.value?.getTotalLength() ?? 0;
});
</script>

WARNING

Pay attention to the behavior in different modes. In the modes loading and determinate the circle fallbacks to the default loader circle. In the future, you might get more control over this behavior.

Below are a few custom examples with SVG shapes taken directly from https://www.svgshapes.in.

vue
<template>
  <ve-progress :progress="50">
    <template #circle-progress="{ attrs }">
      <svg width="200" height="200" fill="none" viewBox="1 -1 51 55">
        <path
          ref="flower"
          :stroke-width="1"
          :stroke="attrs.color"
          :stroke-dashoffset="attrs.calculateProgressOffset(flowerPathLength)"
          :stroke-dasharray="flowerPathLength"
          fill="rgba(255, 251, 0, 1)"
          :style="attrs.baseStyles"
          d="M52.6 26.5c0-2.6-2.1-4.8-4.8-4.8h-3.7l3.2-1.9c2.3-1.3 3.1-4.2 1.7-6.5-1.3-2.3-4.2-3.1-6.5-1.7l-3.2 1.9 1.9-3.2c1.3-2.3.5-5.2-1.7-6.5-2.3-1.3-5.2-.5-6.5 1.7l-1.9 3.2V5c0-2.6-2.1-4.8-4.8-4.8-2.6 0-4.8 2.1-4.8 4.8v3.7l-1.9-3.2c-1.3-2.3-4.2-3.1-6.5-1.7-2.3 1.3-3.1 4.2-1.7 6.5l1.9 3.2-3.2-1.9c-2.3-1.3-5.2-.5-6.5 1.7-1.3 2.3-.5 5.2 1.7 6.5l3.2 1.9H4.8c-2.6 0-4.8 2.1-4.8 4.8 0 2.6 2.1 4.8 4.8 4.8h3.7l-3.2 1.9c-2.3 1.3-3.1 4.2-1.7 6.5 1.3 2.3 4.2 3.1 6.5 1.7l3.2-1.9-1.9 3.2c-1.3 2.3-.5 5.2 1.7 6.5 2.3 1.3 5.2.5 6.5-1.7l1.9-3.2V48c0 2.6 2.1 4.8 4.8 4.8 2.6 0 4.8-2.1 4.8-4.8v-3.7l1.9 3.2c1.3 2.3 4.2 3.1 6.5 1.7 2.3-1.3 3.1-4.2 1.7-6.5l-1.9-3.2 3.2 1.9c2.3 1.3 5.2.5 6.5-1.7 1.3-2.3.5-5.2-1.7-6.5l-3.2-1.9h3.7c2.7 0 4.8-2.1 4.8-4.8Z"
        ></path>
      </svg>
    </template>
  </ve-progress>

  <ve-progress :progress="50">
    <template #circle-progress="{ attrs }">
      <svg width="200" height="200" fill="none" viewBox="-1 -1 60 60">
        <path
          :stroke-width="1"
          :stroke="attrs.emptyColor"
          :style="attrs.baseStyles"
          d="M29.4 54.8c-2.3 0-4.1-4.7-6.3-5.2-2.3-.5-5.9 2.8-7.9 1.7-2-1.1-1.4-6-3.1-7.5-1.7-1.5-6.5-.4-7.8-2.3-1.2-1.9 1.8-5.8 1.1-8-.6-2.1-5.3-3.8-5.3-6.1s4.7-4 5.3-6.1c.7-2.2-2.4-6.1-1.1-8 1.2-1.9 6.1-.8 7.8-2.3 1.7-1.5 1-6.4 3.1-7.5 2-1 5.7 2.3 7.9 1.7C25.3 4.7 27 0 29.3 0c2.3 0 4.1 4.7 6.3 5.2 2.3.5 5.9-2.8 7.9-1.7 2 1.1 1.4 6 3.1 7.5 1.7 1.5 6.5.4 7.8 2.3 1.2 1.9-1.8 5.8-1.1 8 .6 2.1 5.3 3.8 5.3 6.1s-4.7 4-5.3 6.1c-.7 2.2 2.4 6.1 1.2 8-1.2 1.9-6.1.8-7.8 2.3-1.7 1.5-1 6.4-3.1 7.5-2 1-5.7-2.3-7.9-1.7-2.3.5-4 5.2-6.3 5.2Z"
        ></path>
        <path
          ref="flower2"
          :stroke-width="1"
          :stroke="attrs.color"
          :stroke-dashoffset="attrs.calculateProgressOffset(flower2PathLength)"
          :stroke-dasharray="flower2PathLength"
          :style="attrs.baseStyles"
          d="M29.4 54.8c-2.3 0-4.1-4.7-6.3-5.2-2.3-.5-5.9 2.8-7.9 1.7-2-1.1-1.4-6-3.1-7.5-1.7-1.5-6.5-.4-7.8-2.3-1.2-1.9 1.8-5.8 1.1-8-.6-2.1-5.3-3.8-5.3-6.1s4.7-4 5.3-6.1c.7-2.2-2.4-6.1-1.1-8 1.2-1.9 6.1-.8 7.8-2.3 1.7-1.5 1-6.4 3.1-7.5 2-1 5.7 2.3 7.9 1.7C25.3 4.7 27 0 29.3 0c2.3 0 4.1 4.7 6.3 5.2 2.3.5 5.9-2.8 7.9-1.7 2 1.1 1.4 6 3.1 7.5 1.7 1.5 6.5.4 7.8 2.3 1.2 1.9-1.8 5.8-1.1 8 .6 2.1 5.3 3.8 5.3 6.1s-4.7 4-5.3 6.1c-.7 2.2 2.4 6.1 1.2 8-1.2 1.9-6.1.8-7.8 2.3-1.7 1.5-1 6.4-3.1 7.5-2 1-5.7-2.3-7.9-1.7-2.3.5-4 5.2-6.3 5.2Z"
        ></path>
      </svg>
    </template>
  </ve-progress>

  <ve-progress :progress="50">
    <template #circle-progress="{ attrs }">
      <svg width="200" height="200" fill="none" viewBox="-5 -5 210 210">
        <path
          ref="flower3"
          fill="url(#paint0_linear_104_76)"
          :stroke-width="4"
          :stroke="attrs.color"
          :stroke-dashoffset="attrs.calculateProgressOffset(flower3PathLength)"
          :stroke-dasharray="flower3PathLength"
          :style="attrs.baseStyles"
          fill-rule="evenodd"
          d="M128.603 16.335c-12.778-21.78-44.267-21.78-57.045 0l-.464.79A33.07 33.07 0 0 1 42.803 33.46l-.917.006C16.635 33.643.89 60.912 13.363 82.869l.453.797a33.07 33.07 0 0 1 0 32.668l-.453.797c-12.472 21.957 3.272 49.226 28.523 49.403l.917.006a33.072 33.072 0 0 1 28.29 16.334l.465.791c12.778 21.78 44.267 21.78 57.045 0l.464-.791a33.073 33.073 0 0 1 28.291-16.334l.918-.006c25.251-.177 40.995-27.446 28.522-49.403l-.453-.797a33.07 33.07 0 0 1 0-32.668l.453-.797c12.473-21.957-3.271-49.226-28.522-49.403l-.918-.006a33.07 33.07 0 0 1-28.291-16.334l-.464-.791Zm-28.522 133.269c27.395 0 49.604-22.208 49.604-49.604s-22.209-49.605-49.604-49.605c-27.396 0-49.605 22.21-49.605 49.605 0 27.396 22.209 49.604 49.605 49.604Z"
          clip-rule="evenodd"
        ></path>
        <defs>
          <linearGradient
            id="paint0_linear_104_76"
            x1="100.081"
            x2="100.081"
            y1="0"
            y2="200"
            gradientUnits="userSpaceOnUse"
          >
            <stop stop-color="#DF99F7"></stop>
            <stop offset="1" stop-color="#FFDBB0"></stop>
          </linearGradient>
        </defs>
      </svg>
    </template>
  </ve-progress>

  <ve-progress :angle="0" :progress="50">
    <template #circle-progress="{ attrs }">
      <svg
        xmlns="http://www.w3.org/2000/svg"
        viewBox="-4 -40 520 520"
        width="200"
        height="200"
      >
        <path
          ref="hart"
          :stroke-width="10"
          stroke="red"
          :stroke-dashoffset="attrs.calculateProgressOffset(hartPathLength)"
          :stroke-dasharray="hartPathLength"
          :style="attrs.baseStyles"
          d="M471.383 44.578C444.879 15.832 408.512 0 368.973 0c-29.555 0-56.621 9.344-80.45 27.77C276.5 37.07 265.605 48.45 256 61.73c-9.602-13.277-20.5-24.66-32.527-33.96C199.648 9.344 172.582 0 143.027 0c-39.539 0-75.91 15.832-102.414 44.578C14.426 72.988 0 111.801 0 153.871c0 43.3 16.137 82.938 50.781 124.742 30.992 37.395 75.535 75.356 127.117 119.313 17.614 15.012 37.579 32.027 58.309 50.152A30.023 30.023 0 0 0 256 455.516a30.03 30.03 0 0 0 19.785-7.43c20.73-18.129 40.707-35.152 58.328-50.172 51.575-43.95 96.117-81.906 127.11-119.305C495.867 236.81 512 197.172 512 153.867c0-42.066-14.426-80.879-40.617-109.289zm0 0"
          fill='url("#SvgjsLinearGradient1040")'
        ></path>
        <defs>
          <linearGradient id="SvgjsLinearGradient1040">
            <stop stop-color="#d5dee7" offset="0"></stop>
            <stop stop-color="#ffffff" offset="1"></stop>
          </linearGradient>
        </defs>
      </svg>
    </template>
  </ve-progress>
</template>

<script setup>
import { ref, watch, useTemplateRef } from "vue";

const flowerPathLength = ref(0);
const flower2PathLength = ref(0);
const flower3PathLength = ref(0);
const hartPathLength = ref(0);
const flower = useTemplateRef("flower");
const flower2 = useTemplateRef("flower2");
const flower3 = useTemplateRef("flower3");
const hart = useTemplateRef("hart");

watch(flower, () => {
  flowerPathLength.value = flower.value?.getTotalLength() ?? 0;
});
watch(flower2, () => {
  flower2PathLength.value = flower2.value?.getTotalLength() ?? 0;
});
watch(flower3, () => {
  flower3PathLength.value = flower3.value?.getTotalLength() ?? 0;
});
watch(hart, () => {
  hartPathLength.value = hart.value?.getTotalLength() ?? 0;
});
</script>

Released under the MIT License.