DEV Community

Cover image for How to make a confetti cannon with React Spring
Brian Neville-O'Neill
Brian Neville-O'Neill

Posted on • Originally published at blog.logrocket.com on

How to make a confetti cannon with React Spring

Written by Joshua Saunders✏️

You know what everybody loves in their daily lives? A little validation, a little pat on the back, a little celebration — and a little confetti.

In this tutorial, you’ll learn how to implement a confetti cannon that can fire off of any element using React Spring from scratch. No previous React Spring experience required! The only prerequisite is a basic understanding of React and hooks.

Confetti Cannon Built With React Spring

If you want to jump ahead, you can skip to the completed CodeSandbox example.

Note: this tutorial uses styled-components. If you’ve never used styled-components before, don’t sweat it. It’s a powerful library for inline styling of React components, but it’s very readable, so you’ll get the gist of it just by looking at the code.

Game plan

When I’m starting to implement something I’ve never seen before, I like to break it down into phases, starting with the core pieces, then polish afterward. We’ll attack this project step by step:

  1. Get something showing up on the page
  2. Set up React Spring
  3. Write some basic psuedo-physics helpers
  4. Anchor a single dot
  5. Get many dots moving like they’re being fired from a confetti cannon
  6. Add variation to confetti pieces, such as different shapes, colors, and sizes

Let’s get started!

LogRocket Free Trial Banner

1. Something on the page

First, let’s make up a little app. We’ll make it a to-do app and set it to fire confetti from the checkbox when you complete an item.

Basic To-Do App to Demonstrate How to Build a Confetti Cannon in React Spring

Now, let’s add a single confetti dot, which we’ll play with for the next few steps of this tutorial.

const StyledConfettiDot = styled.svg`
  position: absolute;
  will-change: transform;
`;
const Dot = () => (
  <StyledConfettiDot>
    <circle cx="5" cy="5" r="5" fill="blue" />
  </StyledConfettiDot>
);
Enter fullscreen mode Exit fullscreen mode

2. React Spring setup

React Spring is the animation library we’ll be using in this tutorial. It’s a unique library that takes the stance that animations powered by springs rather than keyframes look more natural. Instead of specifying how long an animation is and what changes occur at what time, you specify the tension, friction, and mass of the spring, as well as the start and end values of the animation, and let React Spring figure out how they relate to the spring.

Let’s get React Spring set up with our confetti dot. Run either of the following.

  • npm install react-spring
  • yarn add react-spring

Add the following import to ConfettiDot.js.

import { animated, config, useSpring } from 'react-spring';
Enter fullscreen mode Exit fullscreen mode
  • animated is used to wrap existing components to allow them to use react-spring
  • configs are the preset spring configs that ship with react-spring (we’ll be using the default config)
  • useSpring is one of the main exports from react-spring (there is a handful of other exports, but we’ll be focusing on useSpring)

ConfettiDot enabled with react-spring looks like this:

const AnimatedConfettiDot = animated(StyledConfettiDot);
const Dot = () => {
  const { y } = useSpring({
    config: config.default,
    from: { y: 0 },
    to: { y: -50 }
  });
  return (
    <AnimatedConfettiDot
      style={{
        transform: y.interpolate(yValue => `translate3d(0,${yValue}px,0)`)
      }}
    >
      <circle cx="5" cy="5" r="5" fill="blue" />
    </AnimatedConfettiDot>
  );
};
Enter fullscreen mode Exit fullscreen mode

We’ve used animated to wrap our StyledConfettiDot component. All we have to do is call animated(<component>).

useSpring takes an object with various properties. First, a config object — we’ll use the default one shipped with react-spring since it has no bounceback. Next, a from object that states arbitrary initial values, followed by a to object that states matching end values. The whole hook returns an object that matches the from and to objects. In this example, we’ve set a y initial and end value, and we’re destructing the result to get the y animated value.

Instead of using ConfettiDot or StyledConfettiDot in the render, we’re now using AnimatedConfettiDot, the result of the animated call.

In the style attribute of AnimatedConfettiDot, we use the result of the objects in useSpring to turn the values into valid style values.

Let’s break down the style attribute in more detail. Firstly, we’re using the style attribute instead of props because when the values change, since it’s using animated, it’ll just change the DOM element’s style values as opposed to causing a rerender in React. That means you can have complex animations fully on only one render. Without this, performance would be extremely slow.

Secondly, we’re using the interpolate function on y to convert it into a real string value. For values that are already equal to their final style value, such as a color or percentage value, you wouldn’t need to use interpolate. We’ll demonstrate this later on.

Basic To-Do App to Demonstrate the interpolate Function in React Spring

3. Pseudo-physics

While a circle moving upward is pretty fun, we want it to look like it’s firing out of a confetti cannon. To accomplish this, we’re going to make some pseudo-physics.

  • When the confetti fires out of the cannon, it has a high velocity
  • The confetti slows down quickly
  • Eventually, gravity overtakes its velocity and it begins to fall back down

We’ll use react-spring to simulate the confetti’s velocity at time t. Let’s make a spring that goes from 100 to 0.

const { upwards } = useSpring({
  config: config.default,
  from: { upwards: 100 },
  to: { upwards: 0 },
});
Enter fullscreen mode Exit fullscreen mode

Let’s pretend this velocity represents pixels per second — so, starting at 100 pixels per second to 0 pixels per second.

To actually use this to move the confetti dot, we’ll do the following.

const initialY = 0;
let totalUpwards = 0;
const startTime = new Date().getTime() / 1000;
let lastTime = startTime;

return (
  <AnimatedConfettiDot
    style={{
      transform: upwards.interpolate(upwardsValue => {
        const currentTime = new Date().getTime() / 1000;
        const duration = currentTime - lastTime;
        const verticalTraveled = upwardsValue * duration;
        totalUpwards += verticalTraveled;
        lastTime = currentTime;

        return `translate3d(0, ${initialY - totalUpwards}px, 0)`;
      })
    }}
  >
    <circle cx="5" cy="5" r="5" fill="blue" />
  </AnimatedConfettiDot>
);
Enter fullscreen mode Exit fullscreen mode

This is a fun trick. Since interpolate is called on every tick of react-spring, we’re calculating the time between the current tick and the last tick, getting the current velocity, and calculating the distance traveled (velocity * duration since last tick), then adding that to the total distance traveled in totalUpwards. Then we use totalUpwards as the resulting translated value (using subtraction, since positive upward movement is negative y axis movement in the DOM).

Basic To-Do App to Demonstrate How to Translate velocity Into a translate Value With React Spring

It’s looking great so far! We’ve successfully translated velocity into a translate value. What’s still missing, though, is constant gravity. In terms of physics, that’s easy to implement, since gravity at time t is just t * total time.

const initialY = 0;
  let totalUpwards = 0;
  const startTime = new Date().getTime() / 1000;
  let lastTime = startTime;
  const gravityPerSecond = 30;
  return (
    <AnimatedConfettiDot
      style={{
        transform: upwards.interpolate(upwardsValue => {
          const currentTime = new Date().getTime() / 1000;
          const duration = currentTime - lastTime;
          const verticalTraveled = upwardsValue * duration;
          const totalDuration = currentTime - startTime;
          totalUpwards += verticalTraveled;
          lastTime = currentTime;
          const totalGravity = gravityPerSecond * totalDuration;
          const finalY = initialY - totalUpwards + totalGravity;
          return `translate3d(0, ${finalY}px, 0)`;
        })
      }}
    >
      <circle cx="5" cy="5" r="5" fill="blue" />
    </AnimatedConfettiDot>
  );
};
Enter fullscreen mode Exit fullscreen mode

Changing the initial upward velocity to 300 results in the following.

Basic To-Do App to Demonstrate How to Change Upward Velocity With React Spring

Let’s add horizontal movement as well. It’s a similar mechanism, so I’ll cut to the chase.

const { horizontal, upwards } = useSpring({
    config: config.default,
    from: {
      horizontal: 200,
      upwards: 300
    },
    to: {
      horizontal: 0,
      upwards: 0
    }
  });
  const initialX = 0;
  const initialY = 0;
  let totalUpwards = 0;
  let totalHorizontal = 0;
  const startTime = new Date().getTime() / 1000;
  let lastTime = startTime;
  const gravityPerSecond = 30;
  return (
    <AnimatedConfettiDot
      style={{
        transform: interpolate([upwards, horizontal], (v, h) => {
          const currentTime = new Date().getTime() / 1000;
          const duration = currentTime - lastTime;
          const totalDuration = currentTime - startTime;
          const verticalTraveled = v * duration;
          const horizontalTraveled = h * duration;
          totalUpwards += verticalTraveled;
          totalHorizontal += horizontalTraveled;
          lastTime = currentTime;
          const totalGravity = gravityPerSecond * totalDuration;
          const finalX = initialX + totalHorizontal;
          const finalY = initialY - totalUpwards + totalGravity;
          return `translate3d(${finalX}px, ${finalY}px, 0)`;
        })
      }}
    >
      <circle cx="5" cy="5" r="5" fill="blue" />
    </AnimatedConfettiDot>
  );
Enter fullscreen mode Exit fullscreen mode

Similar to the upward velocity, we’ve added a horizontal velocity spring in existing from and to values and calculated the horizontal distance traveled for each tick of the spring.

The one new thing is that we’re not just interpolating one value anymore, so we need to use the interpolate function exported from react-spring. This function’s first argument is an array of springs, and the second argument is a function that does something with each of the spring values in that array. So in this particular example, the first argument is a list of the upward and horizontal velocity, and the second argument is a function that has upward velocity as its first argument and horizontal velocity as its second argument.

Basic To-Do App to Demonstrate How to Interpolate Multiple Values With React Spring

4. Anchoring

Before we start making many pieces of confetti go flying, let’s make this single piece actually look like it’s coming out of a specific element.

The first step is to make the confetti appear when the checkbox is clicked.

const ToDo = ({ text }) => {
  const [done, setDone] = useState(false);
  return (
    <StyledToDo>
      <input type="checkbox" onChange={() => setDone(!done)} />
      <span>
        {text} {done ? ":ok_hand:" : ""}
      </span>
      {done && <ConfettiDot />}
    </StyledToDo>
  );
};
Enter fullscreen mode Exit fullscreen mode

In each ToDo component, when the done state is true, render a ConfettiDot.

Basic To-Do App to Demonstrate Anchoring With React Spring

It looks like it’s aligned with the checkbox, but if you look closely, you might notice that the animation starts at the top-left of the checkbox. It looks okay, but if it were a different element, such as a text box input, this would look pretty strange.

We’ll use refs to align the animation with the checkbox.

const alignWithAnchor = anchorRef => {
  if (anchorRef.current == null) {
    return {
      initialX: 0,
      initialY: 0
    };
  }
  const { height, width } = anchorRef.current.getBoundingClientRect();
  return {
    initialX: width / 2,
    initialY: height / 2
  };
};

const Dot = ({ anchorRef }) => {
  const { initialX, initialY } = alignWithAnchor(anchorRef);
  // ...
}

const ToDo = ({ text }) => {
  const confettiAnchorRef = useRef();
  const [done, setDone] = useState(false);
  return (
    <StyledToDo>
      <input
        ref={confettiAnchorRef}
        type="checkbox"
        onChange={() => setDone(!done)}
      />
      <span>
        {text} {done ? ":ok_hand:" : ""}
      </span>
      {done && <ConfettiDot anchorRef={confettiAnchorRef} />}
    </StyledToDo>
  );
};
Enter fullscreen mode Exit fullscreen mode

To use the ref, follow these steps:

  1. In ToDo, call useRef()
  2. Attach the resulting ref to the input by using ref={confettiAnchorRef} (now the ref will contain the DOM element of the input)
  3. Pass the ref to ConfettiDot
  4. In ConfettiDot, access the ref and pass it to a helper
  5. In the helper, calculate the middle of the ref element

Now the animation is a little cleaned up.

Basic To-Do App to Demonstrate How to Build a Confetti Cannon React Spring

5. Making the cannon

Now that we’ve got a single confetti dot moving the way we want it to when we want it to, let’s make it a confetti cannon that sprays a randomized fan of confetti. We want our confetti cannon component to:

  • Have an anchor ref prop for alignment
  • Have a vertical range
  • Have a horizontal range
  • Fire a certain number of confetti dots
const ToDo = ({ text }) => {
const confettiAnchorRef = useRef();
const [done, setDone] = useState(false);
return (
// ...
{done && }
);
};const ConfettiCannon = ({ anchorRef, dotCount }) => (
<>
{new Array(dotCount).fill().map((_, index) => ())}
</>
);
Enter fullscreen mode Exit fullscreen mode

Basic To-Do App to Demonstrate How to Build a Confetti Cannon React Spring

It doesn’t look too different, does it? Even though we’re rendering five confetti dots, they all have identical animations, since the confetti dots have their upward and horizontal movement props baked in. Let’s extract those and randomize them within a range.

const randomInRange = (min, max) => {
  return Math.random() * (max - min) + min;
};

const ConfettiCannon = ({ anchorRef, dotCount }) => (
  <>
    {new Array(dotCount).fill().map((_, index) => (
      <ConfettiDot
        key={index}
        anchorRef={anchorRef}
        initialHorizontal={randomInRange(-250, 250)}
        initialUpwards={randomInRange(200, 700)}
      />
    ))}
  </>
);

const Dot = ({ anchorRef, initialHorizontal, initialUpwards }) => {
  const { initialX, initialY } = alignWithAnchor(anchorRef);
  const { horizontal, upwards } = useSpring({
    config: config.default,
    from: {
      horizontal: initialHorizontal,
      upwards: initialUpwards
    },
    to: {
      horizontal: 0,
      upwards: 0
    }
  });

  // ...
}
Enter fullscreen mode Exit fullscreen mode

Now instead of having a baked-in initial horizontal and upward velocity, we’ll randomize each dot. Horizontal velocity goes from -250 to 250 to represent dots flying both left of the anchor and right of the anchor, and the upward velocity goes from 200 to 700. Feel free to play around with these values.

Basic To-Do App to Demonstrate How to Randomize the Dots in a Confetti Cannon Built With React Spring

6. Polish

At this point, we’ve done all the hard work required for this project. To polish it off, we’ll do the following.

  1. Fade out the confetti as it falls
  2. Randomize colors
  3. Randomize shapes
  4. Randomize sizes

Let’s break this down step by step.

Fade out

The confetti should disappear as it nears the end of its animation. To accomplish this, all we need to do is add the following in ConfettiDot.

const Dot = ({ anchorRef, initialHorizontal, initialUpwards }) => {
  const { initialX, initialY } = alignWithAnchor(anchorRef);
  const { horizontal, opacity, upwards } = useSpring({
    config: config.default,
    from: {
      horizontal: initialHorizontal,
      opacity: 80,
      upwards: initialUpwards
    },
    to: {
      horizontal: 0,
      opacity: 0,
      upwards: 0
    }
  });

// ...

  return (
    <AnimatedConfettiDot
      style={{
        opacity,
        transform: interpolate([upwards, horizontal], (v, h) => {
          // ...
        })
      }}
    >
      <circle cx="5" cy="5" r="5" fill="blue" />
    </AnimatedConfettiDot>
  );
}
Enter fullscreen mode Exit fullscreen mode

Since opacity actually returns a number, and that’s what the valid style value is, we don’t need to interpolate it. We can drop it right into the style attribute of AnimatedConfettiDot.

Basic To-Do App to Demonstrate How to Change the Opacity in a Confetti Cannon Built With React Spring

Randomize colors

Blue is fine, but of course, more variance is better. Let’s add a color prop to ConfettiDot, add a colors prop to ConfettiCannon, and randomly pick colors from there to assign to created ConfettiDots.

const Dot = ({ anchorRef, color, initialHorizontal, initialUpwards }) => {
  // ...

  return (
    <AnimatedConfettiDot
      // ...
    >
      <circle cx="5" cy="5" r="5" fill={color} />
    </AnimatedConfettiDot>
  );
}

const randomInRange = (min, max) => {
  return Math.random() * (max - min) + min;
};

const randomIntInRange = (min, max) => Math.floor(randomInRange(min, max));

const ConfettiCannon = ({ anchorRef, colors, dotCount }) => (
  <>
    {new Array(dotCount).fill().map((_, index) => (
      <ConfettiDot
        key={index}
        anchorRef={anchorRef}
        color={colors[randomIntInRange(0, colors.length)]}
        initialHorizontal={randomInRange(-250, 250)}
        initialUpwards={randomInRange(200, 700)}
      />
    ))}
  </>
);
Enter fullscreen mode Exit fullscreen mode

Basic To-Do App to Demonstrate How to Randomize the Colors of the Dots in a Confetti Cannon Built With React Spring

This can be especially useful if you want to stylize your confetti in the brand colors of the app using this library.

Randomize shapes

Circles are also fine, but they don’t look like the most convincing confetti pieces in the world. Let’s randomly make squares and triangles as well.

const Circle = ({ color, size }) => (
  <circle
    cx={`${size / 2}`}
    cy={`${size / 2}`}
    r={`${(size / 2) * 0.6}`}
    fill={color}
  />
);
const Triangle = ({ color, size }) => {
  const flipped = flipCoin();
  return (
    <polygon
      points={`${size / 2},0 ${size},${randomInRange(
        flipped ? size / 2 : 0,
        size
      )} 0,${randomInRange(flipped ? 0 : size / 2, size)}`}
      fill={color}
    />
  );
};
const Square = ({ color, size }) => {
  const flipped = flipCoin();
  return (
    <rect
      height={`${randomInRange(0, flipped ? size : size / 2)}`}
      width={`${randomInRange(0, flipped ? size / 2 : size)}`}
      fill={color}
    />
  );
};
const getRandomShape = color => {
  const Shape = randomFromArray([Circle, Square, Triangle]);
  return <Shape color={color} size={10} />;
};

return (
  <AnimatedConfettiDot
    // ...
  >
    {getRandomShape(color)}
  </AnimatedConfettiDot>
);
Enter fullscreen mode Exit fullscreen mode

Now we’ll randomly get a triangle, square, or circle. The triangle and square have some extra code in them to make sure you never end up with a square that’s just a line or a triangle that’s just a line. I’ve left out the code for flipCoin and randomFromArray from this snippet, but it’s in the CodeSandbox.

Basic To-Do App to Demonstrate How to Randomize the Shapes of the Dots in a Confetti Cannon Built With React Spring

One last thing that’d be nice to polish: as of now, there’s no rotation, which makes it so that each triangle has a point facing directly up, and each rectangle is either fully vertical or fully horizontal. Let’s fix that.

const ConfettiCannon = ({ anchorRef, colors, dotCount }) => (
  <>
    {new Array(dotCount).fill().map((_, index) => (
      <ConfettiDot
        key={index}
        anchorRef={anchorRef}
        color={colors[randomIntInRange(0, colors.length)]}
        initialHorizontal={randomInRange(-250, 250)}
        initialUpwards={randomInRange(200, 700)}
        rotate={randomInRange(0, 360)}
      />
    ))}
  </>
);

const Dot = ({
  anchorRef,
  color,
  initialHorizontal,
  initialUpwards,
  rotate
}) => {
  // ...
  return (
    <AnimatedConfettiDot
      style={{
        opacity,
        transform: interpolate([upwards, horizontal], (v, h) => {
          // ...
          return `translate3d(${finalX}px, ${finalY}px, 0) rotate(${rotate}deg)`;
        })
      }}
    >
      {getRandomShape(color)}
    </AnimatedConfettiDot>
  );
};
Enter fullscreen mode Exit fullscreen mode

Basic To-Do App to Demonstrate How to Randomize Rotation of the Shapes in a Confetti Cannon Built With React Spring

Randomize size

The last aspect to randomize is the size of each dot. Currently, all the dots are the same size, and it’s especially obvious with the circles. Let’s use a similar approach as we did for rotation.

const getRandomShape = (color, size) => {
  const Shape = randomFromArray([Circle, Square, Triangle]);
  return <Shape color={color} size={size} />;
};

const Dot = ({
  anchorRef,
  color,
  initialHorizontal,
  initialUpwards,
  rotate,
  size
}) => {
  // ...
  return (
    <AnimatedConfettiDot
      // ...
    >
      {getRandomShape(color, size)}
    </AnimatedConfettiDot>
  );
};

const ConfettiCannon = ({ anchorRef, colors, dotCount }) => (
  <>
    {new Array(dotCount).fill().map((_, index) => (
      <ConfettiDot
        key={index}
        anchorRef={anchorRef}
        color={colors[randomIntInRange(0, colors.length)]}
        initialHorizontal={randomInRange(-250, 250)}
        initialUpwards={randomInRange(200, 700)}
        rotate={randomInRange(0, 360)}
        size={randomInRange(8, 12)}
      />
    ))}
  </>
);
Enter fullscreen mode Exit fullscreen mode

Basic To-Do App to Demonstrate How to Randomize the Size of the Shapes in a Confetti Cannon Built With React Spring

Conclusion

Congratulations! You’ve made confetti from scratch using React and React Spring. Now you should be much more familiar with using React Spring’s useSpring hook to create powerful and performant animations.

I’ll leave you with these branded confetti cannons!

Basic To-Do App With Branded Confetti Cannons Built With React Spring


Full visibility into production React apps

Debugging React applications can be difficult, especially when users experience issues that are difficult to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

Alt Text

LogRocket is like a DVR for web apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your React apps — start monitoring for free.


The post How to make a confetti cannon with React Spring appeared first on LogRocket Blog.

Top comments (0)