Creating Smooth Animated Counters in Vue 3 with Composables

Creating Smooth Animated Counters in Vue 3 with Composables

ยท

4 min read

In modern web applications, animated counters can significantly enhance user experience by providing visual feedback and making data changes more engaging. In this blog post, we'll explore how to create a reusable animated counter in Vue 3 using composables. We'll build a flexible solution that can animate any numeric property in your Vue store, with customizable duration.

Understanding the Problem

Often, when displaying numerical data that changes frequently, such as search result counts or user statistics, abrupt updates can be jarring to users. A smooth animation between the old and new values can make these changes more pleasant and noticeable.

The Solution: A Custom Composable

Understanding Composables in Vue 3
Composables are a fundamental feature of Vue 3's Composition API. They provide a way to encapsulate and reuse stateful logic across multiple components. Here's a more detailed look at composables:

Key Characteristics of Composables

  • They're Just Functions: Composables are plain JavaScript functions, making them easy to understand and test.

  • They Can Use Composition API Features: Composables can use reactive state, computed properties, watchers, and lifecycle hooks.

  • They Return an Object: Typically, composables return an object containing reactive state and methods.

  • They're Flexible: Composables can accept parameters, allowing for customization.

We'll create a composable function called useAnimatedCount that takes care of the animation logic. This composable will:

  1. Accept a store object, a path to the property we want to animate, and an animation duration.

  2. Return a reactive value that smoothly animates from the current value to the new value whenever the source data changes.

Let's break down the implementation:

useAnimatedCount.js

import { isRef, ref, watch } from "vue"

function getNestedValue(obj, path) {
    const keys = path.split(".")
    let value = obj
    for (const key of keys) {
        if (value && typeof value === "object" && key in value) {
            value = value[key]
        } else {
            return undefined
        }
    }
    return value
}

function setNestedValue(obj, path, value) {
    const keys = path.split(".")
    let target = obj
    for (let i = 0; i < keys.length - 1; i++) {
        const key = keys[i]
        if (!target[key]) target[key] = {}
        target = target[key]
    }
    target[keys[keys.length - 1]] = value
}

export function useAnimatedCount(store, storePropertyPath, duration) {
    const animatedValue = ref(0)
    const durationRef = isRef(duration) ? duration : ref(duration || 500)

    const animateValue = () => {
        const startValue = animatedValue.value
        const endValue = getNestedValue(store, storePropertyPath)
        const startTime = performance.now()

        const step = (currentTime) => {
            const elapsedTime = currentTime - startTime
            const progress = Math.min(elapsedTime / durationRef.value, 1)
            const newValue = Math.floor(startValue + (endValue - startValue) * progress)
            setNestedValue(animatedValue, "value", newValue)

            if (elapsedTime < durationRef.value) {
                requestAnimationFrame(step)
            }
        }

        requestAnimationFrame(step)
    }

    watch(
        () => getNestedValue(store, storePropertyPath),
        () => {
            animateValue()
        }
    )

    watch(durationRef, () => {
        animateValue()
    })

    return animatedValue
}

Key Components of the Solution

  1. Helper Functions: getNestedValue and setNestedValue allow us to work with nested object properties using dot notation.

  2. Reactive References: We use ref to create reactive values for the animated count and duration.

  3. Animation Logic: The animateValue function uses requestAnimationFrame to create a smooth animation between the current and target values.

  4. Watchers: We set up watchers to trigger the animation when either the source data or the duration changes.

Using the Composable

Here's how you can use the useAnimatedCount composable in your Vue component:

<script setup>
import { useAnimatedCount } from './useAnimatedCount'
import { store } from './store'

const searchCount = useAnimatedCount(store, "searchCount", ref(1500))
</script>

<template>
  <div>
    {{ jobSearchCount }}
  </div>
</template>

In this example, we're animating the searchCount property from our store, with an animation duration of 1500 milliseconds.

Benefits of This Approach

  1. Reusability: The composable can be used with any numeric property in your store.

  2. Flexibility: The animation duration can be easily customized and even changed dynamically.

  3. Smooth Updates: Users see a smooth transition between old and new values, improving the overall user experience.

  4. Performance: By using requestAnimationFrame, we ensure that the animation is optimized for browser performance.

Conclusion

Animated counters are a small but impactful way to improve the user experience of your Vue application. By leveraging Vue 3's composition API and creating a custom composable, we've built a reusable solution that can be easily integrated into any component.

This technique not only enhances the visual appeal of your application but also demonstrates the power and flexibility of Vue 3's composition API for creating reusable logic.

Remember, while animations can greatly improve user experience, they should be used judiciously. Always consider the context and ensure that animations enhance rather than distract from the user's primary tasks.

๐Ÿ‘๐Ÿ‘ By coming this far I hope you can implement this awesome counter animated effect on your project. So, I suggest you give it a try on your project and enjoy it!

Feel free to share your thoughts and opinions and leave me a comment if you have any problems or questions.

Till then, Keep on Hacking, Cheers

ย