Skip to content

Custom Progress Indicator

If you want to provide a progress indicator of your own, Wizard of Zod allows you to do that with ease.

Code

vue
<script setup lang='ts'>
import { z } from 'zod'
import Wizard, { type Form } from 'wizard-of-zod'
import ProgressPie from '@/components/progress/ProgressPie.vue'

const forms: Form<z.ZodObject<any>>[] = [
  {
    schema: z.object({
      givenName: z.string().min(2),
      familyName: z.string()
    })
  },
  {
    schema: z.object({
      gender: z.enum(['male', 'female'])
    })
  },
  {
    schema: z.object({
      age: z.number()
    })
  },
]

const handleCompleted = (data: Record<string, any>) => {
  console.log(data)
}
</script>

<template>
  <div class="h-screen flex justify-center items-center">
    <Wizard
      :classes="{
        woz: 'w-1/3',
        wozBody: 'flex-col-reverse',
        wozForm: 'space-y-8',
      }"
      :forms="forms"
      progress-indicator="bar"
      @completed="handleCompleted"
    >
      <template #progressIndicator="{ currentQuestion, totalQuestions, completed }">
        <ProgressPie
          :total-forms="totalQuestions"
          :current-form-index="currentQuestion"
          :completed="completed"
        />
      </template>
    </Wizard>
  </div>
</template>
vue
<script lang="ts" setup>
import { computed, ref, watch } from 'vue'

type Props = {
  totalForms: number
  currentFormIndex: number
  completed: boolean
}

const props = defineProps<Props>()
const piePath = ref('') // Reactive path for the pie chart

// Calculate progress percentage
const progressPercentage = computed(() => {
  if (props.completed) return 100
  const index = Math.max(props.currentFormIndex - 1, 0)
  return (index / props.totalForms) * 100
})

// Generate SVG path for the pie chart based on progress
const calculatePiePath = (percentage: number): string => {
  if (percentage === 100) {
    return `M50,50 m-50,0 a50,50 0 1,1 100,0 a50,50 0 1,1 -100,0 Z`
  }

  const angle = (percentage / 100) * 360
  const largeArcFlag = angle > 180 ? 1 : 0

  const x = 50 + 50 * Math.cos((angle - 90) * (Math.PI / 180))
  const y = 50 + 50 * Math.sin((angle - 90) * (Math.PI / 180))

  return `M50,50 L50,0 A50,50 0 ${largeArcFlag},1 ${x},${y} Z`
}

// Watch progressPercentage to update the piePath with smooth transition
watch(progressPercentage, (newPercentage) => {
  const newPath = calculatePiePath(newPercentage)
  const oldPath = piePath.value

  // Animate transition between paths
  const duration = 500 // Animation duration in ms
  const start = performance.now()

  const animate = (currentTime: number) => {
    const elapsedTime = currentTime - start
    const progress = Math.min(elapsedTime / duration, 1)

    // Interpolate paths (using linear interpolation for simplicity)
    const interpolatedPath = progress < 1 ? oldPath : newPath
    piePath.value = interpolatedPath

    if (progress < 1) {
      requestAnimationFrame(animate)
    }
  }

  requestAnimationFrame(animate)
})

// Initialize piePath with the initial percentage
piePath.value = calculatePiePath(progressPercentage.value)
</script>

<template>
  <div class="flex items-center justify-center">
    <svg :width="100" :height="100" viewBox="0 0 100 100">
      <!-- Background Circle -->
      <circle cx="50" cy="50" r="50" fill="gray" />

      <!-- Foreground Path for Progress -->
      <path :d="piePath" fill="black" />

      <!-- Progress Percentage Text -->
      <text
        x="50"
        y="55"
        text-anchor="middle"
        font-size="16"
        fill="white"
      >
        {{ Math.round(progressPercentage) }}%
      </text>
    </svg>
  </div>
</template>

INFO

The wrapping <div> and classes prop are just used here for presentation purposes. You should omit them or use your own values.

Screenshot


Custom Progress Indicator Example

Resulting Data

javascript
{
  givenName: 'John',
  familyName: 'Doe',
  gender: 'male',
  age: 44
}

Released under the MIT License.