Animated Pride Flags

Introduction

It's June, which means it's Pride Month! Let's celebrate by building a wavy pixellated pride flag:

There's a lot of exciting stuff packed into this tutorial. In order to build this flag, we'll need to rely on a handful of tricks I've developed over years of experimentation. You'll learn a ton about keyframe animations, linear gradients, and more. πŸ˜„

Link to this heading
Show me the code!

Let's start by looking at a complete implementation. It doesn't have all of the bells and whistles we'll add later, but it shows the fundamental idea.

Don't worry if you can't make much sense of it yet, we'll dig into it in this blog post!

Code Playground

Open in CodeSandbox
import React from 'react';
import range from 'lodash.range';

import styles from './PrideFlag.module.css';

function PrideFlag({
  numOfColumns = 10,
  staggeredDelay = 100,
}) {
  return (
    <div className={styles.flag}>
      {range(numOfColumns).map((columnIndex) => (
        <div
          key={columnIndex}
          className={styles.column}
          style={{
            animationDelay: columnIndex * staggeredDelay + 'ms',
          }}
        />
      ))}
    </div>
  );
}

export default PrideFlag;

Link to this heading
The fundamental strategy

Here's how this effect works: our flag consists of several equal-width columns. Each column moves up and down, thanks to a CSS keyframe animation:


            
css

            
            

With this CSS in place, we have a bunch of columns moving up and down. The final missing piece is animation-delay; each column will receive a slightly-larger value. By staggering the animation, we create the illusion of a rippling flag.

Here's a simplified demo. Drag the β€œStaggered Delay” slider to see the effect at work:

To do this, we'll apply increasingly-large values for animation-delay in an inline style:


            
html

            

We can also do this dynamically. Here's the approach I took with React, using the array index to calculate the amount of delay:


            
jsx

            

Link to this heading
Drawing flag bars

So, each column is going up and down, but to complete the illusion, they need to have the colored stripes!

My first thought was to create a bunch of divs, one for each color:

This works, but it winds up creating a lot of DOM nodes:


            
html

            

An 8-color flag with 16 columns produces 128 DOM nodes. For reference, Google recommends that the entire page should contain 1500 or fewer nodes. It feels pretty indulgent to use almost 10% of our total DOM node allotment on this flag animation!

Fortunately, I have another trick up my sleeve: linear gradients.

I always forget that this is an option, because it feels so counter-intuitive. Gradients are used to smoothly fade from one color to another, not to create solid bars!

For example, suppose we're building the super-pretty pansexual flag:

If we plop the colors into a gradient, we get something like this:

Code Playground

Open in CodeSandbox
.flag {
  width: 200px;
  aspect-ratio: 3 / 2;
  background: linear-gradient(
    to bottom,
    hsl(331deg 100% 55%), /* pink */
    hsl(50deg 100% 50%),  /* yellow */
    hsl(200deg 100% 55%)  /* blue */
  );
}

As expected, the colors bleed into each other, creating a smooth fade. Doesn't look much like our pan pride flag!

But check out what happens when we duplicate the colors, and position them strategically using color stops:

Each of the 3 colors is duplicated, and then positioned right up against each other. The pink color spans the first 1/3rd, and then we transition immediately to yellow. Essentially, the pink-to-yellow fade happens over 0px, and therefore, we get solid bars of color.

Here's what this looks like in CSS:


            
css

            
            

With that done, I think we've covered all of the fundamentals! Once again, here's the result:

So, that's the β€œMVP? version” of our flag animation. We're generating a bunch of super-narrow flags using a linear-gradient, and moving them up and down using a CSS keyframe animation.

That said, I have a few more tips and tricks we can use to make this animation even better!

Link to this heading
Controlling the amount of billow

So, here's something that had befuddled me for a long time.

The actual CSS transform is currently hardcoded within our keyframe animation:


            
css

            

What if we wanted this number to be dynamic? For example, wouldn't it be cool if each column had a slightly different billow amount? Like a real flag attached to a flagpole?

It turns out, we can do this with

Here's the end result: a new β€œbillow” parameter that affects how billowy the flag is:

To set this up, we'll need to replace our hardcoded value with a CSS variable, --billow:


            
css

            

Next, we'll define the --billow property in our markup, picking an increasingly-large number for each one:


            
html

            

In React, we can calculate this dynamically, much like we calculate the animationDelay:


            
jsx

            

The .column class is the one that applies the oscillate keyframe animation, and so when the animation runs, it'll read the --billow value from that same DOM node. Because each <div> sets a different value for --billow, we wind up with this beautiful billowy effect!

Thanks to Jez McKean for the suggestion!

CSS variables are incredible. Unlike the variables built into CSS preprocessors (like Sass or Less), CSS variables don't compile away, and can be dynamically modified using JS. This β€œOne Neat Trick” allows us to pass data from JavaScript/React into our CSS keyframe animation. ✨

Link to this heading
Generating the gradient

In the example above, I manually wrote out the linear-gradient for the pansexual flag:


            
css

            

This works, but it's a bit tedious to calculate it by hand. Ideally, our <PrideFlag> component should be able to generate this gradient dynamically, based on the supplied colors!

Here's a JavaScript function that will do this for us:


            
js

            

Link to this heading
Rounded corners

I think our flag will appear much friendlier if it has slightly rounded corners.

This is a surprisingly tricky thing: our flag is actually built out of several identical columns, a collection of super-narrow mini-flags. It'll look really funky if we round the corners of all columns!

I hate this so much!

Instead, we want to selectively apply specific rounding to specific columns. Here's the CSS:


            
css

            

Using the :first-child and :last-child pseudo-classes, we can select the first/last columns in the group, and round the appropriate corners.

Link to this heading
Hiding the initial setup

You may have noticed, in our MVP, that the first second or so is a bit awkward:

Each subsequent column has an increasingly large animation-delay. The final column just sits there for a full second, before the time elapses and it starts oscillating.

In some cases, this won't matter. If the flag is below the fold, for example, the animation should be running smoothly by the time the user scrolls to it. But what if it's above the fold, immediately visible?

It turns out, we can use a negative value for animation-delay!

For example, if we set animation-delay: -200ms, the animation will run immediately, but it will act as though it has already been running for 200ms.

Imagine a car race, except every car has its own starting line staggered along the track. The moment the race starts, each car will be at a different point in the track.

Here's how we should structure things:


            
html

            

There's still a 100ms difference between each column's animation-delay, but they're all less than or equal to zero, so that when the animation starts, each column is at a different point in the oscillation.

In React, we need to calculate these numbers dynamically. Here's the code I used:


            
jsx

            

Link to this heading
Pixel-rounding quirk

Depending on your browser and monitor, you might've noticed a thin gap between columns:

This happens because of a pixel-rounding issue.

In this example, the flag has a width of 200px, and it has 12 columns. When we do the math, we discover that each column is 16.666px wide.

Chrome handles this gracefully, but Firefox and Safari occasionally struggle to fit the columns together seamlessly. As a result, we get a single-pixel gap between certain columns.

How do we fix it? I think the cleanest approach is to tweak the flag's width so that there are no fractional columns. Instead of having 16.666px for each column, what if we round up to 17px? This means our flag will be 204px wide, rather than 200px.

Here's a little JS snippet we can use to calculate this width automatically:


            
js

            

This will pick the closest value to the desired width, for the specified number of columns.

Link to this heading
Putting it all together

Here's the final implementation, using the techniques we've discussed:

Code Playground

Open in CodeSandbox
import React from 'react';
import range from 'lodash.range';

import styles from './PrideFlag.module.css';
import { COLORS } from './constants';

function PrideFlag({
  variant = 'rainbow', // rainbow | rainbow-original | trans | pan
  width = 200,
  numOfColumns = 10,
  staggeredDelay = 100,
  billow = 2,
}) {
  const colors = COLORS[variant];

  const friendlyWidth =
    Math.round(width / numOfColumns) * numOfColumns;

  const firstColumnDelay = numOfColumns * staggeredDelay * -1;

  return (
    <div className={styles.flag} style={{ width: friendlyWidth }}>
      {range(numOfColumns).map((columnIndex) => (
        <div
          key={columnIndex}
          className={styles.column}
          style={{
            '--billow': columnIndex * billow + 'px',
            background: generateGradientString(colors),
            animationDelay:
              firstColumnDelay + columnIndex * staggeredDelay + 'ms',
          }}
        />
      ))}
    </div>
  );
}

function generateGradientString(colors) {
  const numOfColors = colors.length;
  const segmentHeight = 100 / numOfColors;

  const gradientStops = colors.map((color, index) => {
    const from = index * segmentHeight;
    const to = (index + 1) * segmentHeight;

    return `${color} ${from}% ${to}%`;
  });

  return `linear-gradient(to bottom, ${gradientStops.join(', ')})`;
}

export default PrideFlag;

Link to this heading
Happy Pride Month!

I'm so thrilled to get this blog post out β€” I've had this idea for years, but I wanted to ship it during Pride Month, and I kept remembering too late. πŸ˜…

I'm a cis gay man in my 30s, and I've gotten to see so many countries around the world become more accepting of who I am. Canada is one of over 30 countries to have legalized same-sex marriage. 25 years ago, it was illegal everywhere in the world!

It's been wonderful to see my sexual orientation become a normal part of society. At the same time, though, progress has been much slower for trans folks. It seems like a lot of hate has shifted from sexual orientation to gender identity.

Halli summarizes this well:

Trans folks are just trying to live their lives, and it's outrageous that they've become the new queer bogeyman. 😬

If you have any trans friends or family members, I hope you'll offer your unconditional support to them. I also hope you'll consider donating to queer charities (my go-to charity is The Trevor Project, a group that provides free 24/7 access to crisis counselors for LGBTQIA+ youth).

Thanks for reading! I hope you have an excellent Pride Month. πŸ³οΈβ€πŸŒˆπŸ³οΈβ€βš§οΈ


Last Updated

June 8th, 2023

Hits

A front-end web development newsletter that sparks joy

My goal with this blog is to create helpful content for front-end web devs, and my newsletter is no different! I'll let you know when I publish new content, and I'll even share exclusive newsletter-only content now and then.

No spam, unsubscribe at any time.



If you're a human, please ignore this field.