On a Blind Date with React: Part 3 — Building a Radial Progress Indicator

In the series On a Blind Date with React, I’m taking you on a journey in building the online interactive documentary Blind Date with React. We’re coding a video player, overcoming animation challenges and incorporating user research feedback. We’ll transcode media files, hunt bugs and display translations in the browser along the way.

Previous parts: Part 1 — Setting the Stage, Part 2 — Animating the Match screen with GreenSock.

Today we’re going to create a Radial Progress Indicator: a circle filling up to show the progress of video playback. You can see it in action on the Blind Date Match screen (click on one of the foreigners or one of the natives to see the progress indicator fill up as the video plays). It is also used as a countdown between two videos, for example at the end of this video.

This is what we’ll be building:

The Radial Progress Indicator we’re building

We’ll be using:

  • React, as this is a series on building an interactive documentary with React
  • GreenSock, for animating
  • SVG, as it suits our purposes perfectly

Laying the groundwork

The Radial Progress Indicator is a React component that renders an SVG circle. We’ll pass in props for the width, the stroke width and the stroke color. With these we render a transparent circle with a stroke. The width will be the full width of the circle, including the stroke. Just like CSS box-sizing: border-box. Also, because we only deal with perfect circles, the height will be equal to the width.

The stroke is drawn centered around the path it is applied to (source). In other words: the stroke is applied half inside and half outside the circle we’re drawing. We need to adjust the radius so that the stroke appears to be drawn on the outside of the circle.

This is what we got so far. This looks like the 100% progress case, doesn’t it?

The groundwork for the Radial Progress Indicator

Expressing progress on a circle

In order to show the progress on the SVG circle we need a way to influence how much of the stroke is drawn. Enter the stroke-dasharray and stroke-dashoffset attributes.

With stroke-dasharray we can define the stroke to have gaps: just like border-style: dashed in CSS. Unlike CSS’s border-style, with stroke-dasharray we can specify how large the gaps between the dashes should be.

With stroke-dashoffset we can move the position of the dashes.

Combining stroke-dasharray and stroke-dashoffset is often done in SVG line animations. Chris Coyier wrote an excellent explanation in the CSS Tricks article How SVG Line Animation Works.

Here’s the crux: we need to set the stroke-dasharray to equal the length of the stroke of the circle, so we’ll end up with one dash covering the entire circle stroke. Imagine this as a straight line. We then set stroke-dashoffset to equal the length of the stroke of the circle, in effect positioning the dash to start at the end of the line.

How do we calculate the length of the stroke of a circle again? No worries, I had to look it up too. The length of the stroke of a circle is called the circumference and we can calculate it using the radius and Pi:

circumference = 2 * radius * Pi

We set both stroke-dasharray and stroke-dashoffset to the circumference. Then we can decrease the stroke-dashoffset from this initial value to 0 to express progress on the line:

Expressing progress on a circle. For demo purposes I’ve added a couple of buttons to influence the progress and I added the ability to pass the current progress in via a prop.

A little but important detail: we’ve rotated the circle by 270 degrees in order to let the stroke start at the top and increase clockwise.

Animating progress over time

Now that we’ve laid the groundwork and are able to express the progress on a circle, it’s time to animate it!

We’re using GreenSock because it allows us to pause and resume the animation playback later on. For starters, we’ll animate the progress when the RadialProgressIndicator component mounts.

In order to animate progress over time we need to know the desired duration, so we’ll pass in a duration prop. This is the time the progress is animated from 0% to 100%.

First, we add a ref to the circle, so we can pass it to GreenSock for animation. Then we create a new TimelineLite and add a tween (animation) to it, telling it to animate stroke-dashoffset from its initial value to 0 during the specified duration. Also, we want to animate from o% to 100% in a linear fashion, so we define a linear easing.

Now when we render a RadialProgressIndicator it animates:

Animated progress!

Controlling playback

In the Match screen we want to be able to play and pause the video of the selected person. Of course, the progress indicator reflect this state as well. After all, there is no fun in a progress indicator that keeps animating progress when the video is paused!

In good React fashion, we first add a prop to our RadialProgressIndicator component: paused to indicate whether or not the progress is paused. We‘ll use this paused prop to either play or pause the animation timeline.

And that’s all there is to it. Here you can see our RadialProgressIndicator in full glory:

The RadialProgressIndicator with playback control

Next week, we’ll turn the page towards the Video screen. We’ll take a deep dive into the CSS object-fit property to make sure our videos display without any letterboxing (black borders at the top/bottom or left/right).

In the mean time, be sure to visit the site, match a foreigner and a native and enjoy some of their conversations!