26

I'm looking to animate number changes using VueJs.

For example I have:

{{ number }}

Then number changes from 0 to 100, I would like the element to count up to 100 rather than just jumping stright to it.

How would I do this without using any 3rd party (pure Js/VueJs) excluding VueJs?

2
  • Started to answer it but I'm headed out for the night. Here's what I was working on: jsfiddle.net/5nobcLq0/1 Basically, the idea is to keep the number separate from the displayed number, and if they are different increment the display number towards the real number. I think ideally this would be a component of its own, or a directive like v-animate. I'll look at it more tomorrow Commented Feb 21, 2016 at 4:06
  • I had a look at v-animate but couldn't find anything that would help me Commented Feb 21, 2016 at 11:41

6 Answers 6

51

Got this working as a custom component: https://jsfiddle.net/5nobcLq0/5/

html

<body>
  <input v-model="number">
  <animated-number :number="number"></animated-number>
</body>

js

Vue.component('animated-number', {

  template:"{{ displayNumber }}",
  props: {'number': { default:0 }},

  data () {
    return {
      displayNumber:0,
      interval:false
    }
  },

  ready () {
    this.displayNumber = this.number ? this.number : 0;
  },

  watch: {
    number () {
      clearInterval(this.interval);

      if(this.number == this.displayNumber) {
        return;
      }

      this.interval = window.setInterval(() => {
        if(this.displayNumber != this.number) {
          var change = (this.number - this.displayNumber) / 10;
          change = change >= 0 ? Math.ceil(change) : Math.floor(change);
          this.displayNumber = this.displayNumber + change;
        }
      }, 20);
    }
  }
})

new Vue({
  el:'body',
});
Sign up to request clarification or add additional context in comments.

5 Comments

Thank you, this was exactly what I was looking for!
would you be able to adjust this to support decimals? Currently if the number isn't whole it flickers and jumps up and down
the line change = change >= 0 ? Math.ceil(change) : Math.floor(change); uses Math.floor and Math.ceil to round the change off. Just remove that line and it should work. You may want to round to a certain number of decimals instead.
I'd suggest using requestAnimationFrame() instead of setInterval()
You may want to change ready() to mounted() for Vue2 and Vue3. Other than that it works great
14

I realize this is an older post, but I was looking for something similar and I found an example straight from the vue.js 2.0 documentation. You can find it here: https://v2.vuejs.org/v2/guide/transitioning-state.html#Organizing-Transitions-into-Components

I've recreated it in the snippet below.

// This complex tweening logic can now be reused between
// any integers we may wish to animate in our application.
// Components also offer a clean interface for configuring
// more dynamic transitions and complex transition
// strategies.
Vue.component('animated-integer', {
  template: '<span>{{ tweeningValue }}</span>',
  props: {
    value: {
      type: Number,
      required: true
    }
  },
  data: function() {
    return {
      tweeningValue: 0
    }
  },
  watch: {
    value: function(newValue, oldValue) {
      this.tween(oldValue, newValue)
    }
  },
  mounted: function() {
    this.tween(0, this.value)
  },
  methods: {
    tween: function(startValue, endValue) {
      var vm = this

      function animate() {
        if (TWEEN.update()) {
          requestAnimationFrame(animate)
        }
      }
      new TWEEN.Tween({
          tweeningValue: startValue
        })
        .to({
          tweeningValue: endValue
        }, 500)
        .onUpdate(function() {
          vm.tweeningValue = this.tweeningValue.toFixed(0)
        })
        .start()
      animate()
    }
  }
})
// All complexity has now been removed from the main Vue instance!
new Vue({
  el: '#example-8',
  data: {
    firstNumber: 20,
    secondNumber: 40
  },
  computed: {
    result: function() {
      return this.firstNumber + this.secondNumber
    }
  }
})
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/tween.js"></script>
<div id="example-8">
  <input v-model.number="firstNumber" type="number" step="20"> +
  <input v-model.number="secondNumber" type="number" step="20"> = {{ result }}
  <p>
    <animated-integer v-bind:value="firstNumber"></animated-integer> +
    <animated-integer v-bind:value="secondNumber"></animated-integer> =
    <animated-integer v-bind:value="result"></animated-integer>
  </p>
</div>

I hope that helps! Tim

2 Comments

You might want to consider using this.$nextTick rather than requestAnimationFrame, since it is integrated into the VueJs dom update process.
Hi @BenjaminEllis, I just copied the example from the vue.js 2.x docs. So maybe there's a reason for using requestAnimationFrame, otherwise I would assume they would have used it in the example?
3

If you use GSAP

I took Jeff's answer and simplified it using GSAP.

import gsap from 'gsap';

Vue.component('animate-integer', {
  template: '<span>{{displayValue}}</span>',
  props: { value: { default: 0 } },

  data() {
    return {
      displayValue: this.value,
      tweenValue: this.value
    };
  },

  watch: {
    value() {
      gsap.to(this, {
        tweenValue: this.value,
        onUpdate: () => {
          this.displayValue = Math.ceil(this.tweenValue);
        }
      });
    }
  }
});

Usage:

<input v-model="value">
<animate-integer :value="value"></animated-integer>

Update

As I've used this component it evolved to accept duration and formatting, here is a gist.

4 Comments

How about without the input and if we wanna animate after page load?
@OutForCode The input isn't necessary, I put it there as an example. Pass in any value you want, ` <animate-integer :value="someValue"></animated-integer>`, and anytime that value changes, the component animates. If that is more clear I'll update the answer.
I can get it. But I am looking for a load time animate. Value is not going to change once the page is loaded or into view, it should animate from 0 to the value. Can you please help me with that?
You can do something like :value="inView ? myValue : 0". inView is false until you are ready to animate, then set it to true.
3

If you want a vue 3 composition API component, based on @jeff's & @AnandShiva's answers. Here it is:

<template>{{ displayNumber }}</template>

<script setup>
const props = defineProps({
  value: {
    type: Number,
    default: 0,
  },
  speed: {
    // smaller is faster
    type: Number,
    default: 5,
  },
});

const displayNumber = ref(props.value);
let interval = null;

watch(
  () => props.value,
  (newVal) => {
    clearInterval(interval);

    if (newVal === displayNumber.value) {
      return;
    }

    interval = setInterval(() => {
      if (Math.floor(displayNumber.value) !== Math.floor(newVal)) {
        var change = (newVal - displayNumber.value) / props.speed;
        change = change >= 0 ? Math.ceil(change) : Math.floor(change);
        displayNumber.value = displayNumber.value + change;
      } else {
        displayNumber.value = newVal;
        clearInterval(interval);
      }
    }, 20);
  }
);
</script>

Comments

1

If you want support for the animation of floating-point numbers and negative numbers also this vue component will work. It's built on top of @jeff's answer. Change the number 5 or make it a prop to control how fast or slow the animation happens.

<template>
  <span>{{displayNumber}}</span>
</template>

<script>

export default {
  data () {
    return {
      displayNumber: 0,
      counter: false
    };
  },
  props: {
    number: {
      type: Number,
      default: 0
    }
  },
  watch: {
    number () {
      clearInterval(this.counter);
      if (this.number === this.displayNumber) {
        return;
      }
      this.counter = setInterval(function () {
        if (Math.floor(this.displayNumber) !== Math.floor(this.number)) {
          var change = (this.number - this.displayNumber) / 5;
          change = change >= 0 ? Math.ceil(change) : Math.floor(change);
          this.displayNumber = this.displayNumber + change;
        } else {
          this.displayNumber = this.number;
          clearInterval(this.counter);
        }
      }.bind(this), 20);
    }
  }
};
</script>


Comments

0

I improved [@AnandShiva's][1] Script it appears there were 3-4 edge cases which needed proper handling:

  • The component properly handles decimal places based on the new value (Previously when transitioning from 0.33 to 0.3344 there could appear 0.033xxxxxxxxxxxx numbers now it is limited to 0.33xx - The new number digit count determines maximum allowed digits after zero)

  • There is a need to disable animations in some cases so animate=true/false added

  • Zero case was not handled, the component now ensures proper handling of zero

  • Sometimes APIs provide string inputs also instead of floats or integers so component now also accepts strings

Usage:

<template>
  <div>
    <animated-number :value="currentNumber" :animate="true"></animated-number>
  </div>
</template>

<script>
import AnimatedNumber from './AnimatedNumber.vue';

export default {
  components: {
    AnimatedNumber
  },
  data() {
    return {
      currentNumber: 123.45
    };
  },
  mounted() {
    // Example of changing the number after 2 seconds
    setTimeout(() => {
      this.currentNumber = 678.90;
    }, 2000);
  }
};
</script>

And the component itself:

<template>
  <span>{{ formattedNumber }}</span>
</template>

<script>
export default {
  data() {
    return {
      displayNumber: 0,
      counter: null,
      maxDecimalPlaces: 0
    };
  },
  props: {
    value: {
      type: [Number, String],
      default: 0
    },
    animate: {
      type: Boolean,
      default: true
    }
  },
  watch: {
    value() {
      const newValue = parseFloat(this.value) || 0; // Parse the value to a number, default to 0 if parsing fails
      this.maxDecimalPlaces = this.getDecimalPlaces(newValue);
      // console.log('maxDecimalPlaces', this.maxDecimalPlaces);
      // console.log('Number prop changed:', newValue);
      if (!this.animate) {
        this.displayNumber = newValue;
        return;
      }

      clearInterval(this.counter);
      if (newValue === this.displayNumber) {
        return;
      }
      this.counter = setInterval(() => {
        let change = (newValue - this.displayNumber) / 5;
        this.displayNumber += change;

        if (Math.abs(newValue - this.displayNumber) < Math.pow(10, -this.maxDecimalPlaces)) {
          this.displayNumber = newValue;
          clearInterval(this.counter);
        }
      }, 20);
    }
  },
  computed: {
    formattedNumber() {
      return this.formatNumber(this.displayNumber);
    }
  },
  methods: {
    getDecimalPlaces(number) {
      const parts = number.toString().split('.');
      return parts.length > 1 ? parts[1].length : 0;
    },
    formatNumber(number) {
      if (number === 0) return '0'; // Explicitly handle the case where the number is 0
      return number.toFixed(this.maxDecimalPlaces).replace(/\.?0+$/, '');
    }
  },
  mounted() {
    const initialValue = parseFloat(this.value) || 0; // Parse the initial value to a number, default to 0 if parsing fails
    this.maxDecimalPlaces = this.getDecimalPlaces(initialValue);
    this.displayNumber = initialValue;
    // console.log('Component mounted, initial displayNumber:', this.displayNumber);
  }
};
</script>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.