Post

How to Create a Live Countup Animation with Ruby on Rails and Stimulus

How to Create a Live Countup Animation with Ruby on Rails and Stimulus

Introduction

In this tutorial, we’ll walk through the process of creating a simple yet effective count-up animation using Ruby on Rails and Stimulus.js. This technique can be particularly useful when you want to dynamically animate numbers on your webpage, such as sales figures, user statistics, or revenue.

We’ll explore how to build this functionality from scratch using Stimulus controllers and data- attributes to keep your code organized and maintainable.

Project Setup

HTML

First, let’s define the HTML for displaying the stats.

1
2
3
4
<div data-controller="count-up" data-count-up-sales-end-value="1000">
  <p>Sales Tracked:</p>
  <div data-count-up-target="sales">0</div>
</div>
  • We are using the data-controller="count-up" attribute to tell Stimulus that this element will be controlled by the count_up_controller.js that we’ll create.
  • data-count-up-end-value="1000" defines the target number to which the counter will animate.
  • Inside the <div>, the data-count-up-target="counter" is the element that will display the animated value. This is where the counter starts at 0 and increments to the target value (1000 in this case).

Now that we have the basic HTML structure, let’s move on to the JavaScript part, where we’ll define the count-up functionality using Stimulus.

Stimulus

Generating the controller

Let’s create the Count Up Controller that will handle the animation logic. This controller will be able to handle the counting up of multiple elements such as sales, users, or revenue.

1
rails generate stimulus count_up

This will create a new Stimulus controller file located in app/javascript/controllers/count_up_controller.js, where we will add our custom logic for the count-up animation.

Implementing the logic

Here is how the count_up_controller.js should look. The code is explained below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = [ "sales" ]
  static values = {
    salesEnd: Number,
  }

  connect() {
    this.startCountUp();
  }

  startCountUp() {
    const duration = 2000; // Duration of the animation in milliseconds
    this.animateCount(this.salesTarget, this.salesEndValue, duration);
  }

  animateCount(target, endValue, duration) {
    const startValue = 0;
    const startTime = performance.now();

    const animate = (currentTime) => {
      const elapsedTime = currentTime - startTime;
      const progress = Math.min(elapsedTime / duration, 1);
      const currentValue = Math.floor(startValue + (endValue - startValue) * progress);

      target.textContent = Math.max(currentValue, 0); // Ensure it doesn't go below 0

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

    requestAnimationFrame(animate);
  }
}
1. Import and Controller Definition
1
2
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {...}

This imports the base Controller class from Stimulus, allowing us to extend it and create a custom controller.

2. Static Targets and Values
1
2
3
4
static targets = [ "sales" ]
static values = {
  salesEnd: Number,
}
  • static targets: Defines a target named sales, which will be the HTML element that displays the number being counted up.
  • static values: Defines a value salesEnd, which will represent the final value the count-up animation will reach. This value is passed dynamically via HTML.
3. The connect() Method
1
2
3
connect() {
  this.startCountUp();
}

The connect() method is automatically called when the controller is connected to the DOM (i.e., when the page loads or the element it is attached to is rendered). In this case, it calls startCountUp() when the controller is initialized.

4. The startCountUp() Method
1
2
3
4
startCountUp() {
  const duration = 2000; // Duration of the animation in milliseconds
  this.animateCount(this.salesTarget, this.salesEndValue, duration);
}
  • duration: The duration of the count-up animation (2 seconds in this example).
  • animateCount(): This method is invoked to start the actual counting animation. It passes three arguments:
  • this.salesTarget: Refers to the HTML element that will display the animated number (the sales target).
  • this.salesEndValue: The end value for the count-up (e.g., 1000 sales).
5. The animateCount() Method
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  animateCount(target, endValue, duration) {
    const startValue = 0;
    const startTime = performance.now(); // Get the current time when animation starts

    const animate = (currentTime) => {
      const elapsedTime = currentTime - startTime;
      const progress = Math.min(elapsedTime / duration, 1); // Ensure progress is between 0 and 1
      const currentValue = Math.floor(startValue + (endValue - startValue) * progress); // Calculate current value

      target.textContent = Math.max(currentValue, 0); // Update the text with the current value

      if (progress < 1) {
        requestAnimationFrame(animate); // Continue the animation
      }
    };

    requestAnimationFrame(animate); // Start the animation loop
  }

How animateCount() works:

  • startValue: The count starts from 0.
  • startTime: The exact time the animation begins (via performance.now()).

Inside animate():

  • elapsedTime: The time elapsed since the animation started.
  • progress: A fraction of how far along the animation is (from 0 to 1). It’s the elapsed time divided by the total duration.
  • currentValue: The current value to be displayed, calculated as a linear interpolation between the start and end values based on progress.
  • target.textContent = currentValue: Updates the target element’s text content to show the current count.

If the progress is still less than 1 (i.e., the animation is not finished), it recursively calls requestAnimationFrame(animate) to keep updating the number until the final value is reached.

requestAnimationFrame is used for smooth animations. It tells the browser to execute the animate() function before the next repaint, ensuring that the animation is smooth and efficient.

Conclusion

This Stimulus controller animates the counting process for a specified target (in this case, sales). The final value and the duration are passed dynamically, and the controller ensures smooth animation using requestAnimationFrame().

This structure allows you to easily extend the controller to handle multiple count-up elements by adding more targets or values, such as users or revenue, with the same animation logic.



PS. Not sure if this is the best way to do this, but it worked for me.

This post is licensed under CC BY-NC 4.0 by the author.