A CSS Approach to Animated Pride Flags

June is Pride Month, and it's a great time to build a wavy, pixellated pride flag. The underlying technique relies on some clever use of CSS keyframe animations and linear gradients, and it's a fun way to learn about a few front-end tricks.

Building the Flag: Columns and Keyframes

Instead of building a flag as a single block, the visual is constructed from several equal-width columns. The core idea is simple: each column runs a CSS keyframe animation that moves it up and down. To create the illusion of a ripple, we stagger the start of each column's animation by assigning a progressively larger animation-delay value via inline styles. With enough columns and a well-tuned delay, these simultaneous oscillations look like a flag flapping in the wind.

.column {
  animation: oscillate 1s ease-in-out infinite;
}

@keyframes oscillate {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-10px); }
}

In practice, the delay for each column can be calculated dynamically based on its index within the array of columns.

Creating Stripes with Linear Gradients

Adding the colored stripes to each column is the next challenge. While it might seem intuitive to render each stripe as a separate div, doing so multiplies the DOM node count very quickly. A flag with eight colors and sixteen columns would produce 128 nodes, which is a huge chunk of the recommended total of 1500 nodes for an entire page.

A better strategy comes from an unexpected place: the linear-gradient function. Although gradients are typically used for smooth fades, we can control them precisely to create solid vertical bars. The trick is to duplicate each color stop in the gradient definition and position them so the transition happens over an imperceptibly small space. For instance, using color stops like color1 0%, color1 33%, color2 33%, color2 66%, color3 66%, color3 100% results in three hard-edged bars rather than a smooth blend. This keeps the DOM structure extremely light, consisting only of the columns themselves.

background-image: linear-gradient(
  to bottom,
  #ff218c 0%,
  #ff218c 33%,
  #ffd800 33%,
  #ffd800 66%,
  #21b1ff 66%,
  #21b1ff 100%
);

Refining the Animation

Controlling the Billow Effect

The basic keyframe animation has a fixed travel distance. To make the effect more realistic, we can let each column billow by a different amount, much like a real flag attached to a pole. This requires making the CSS transform scale dynamic. The solution is to use a custom CSS property, often called a CSS variable. By using a variable like --billow in the keyframe animation, we can then override it on each individual column. Doing so lets each column inherit a unique, computed value for the oscillation amplitude, which creates a much richer wave pattern.

@keyframes oscillate {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(var(--billow)); }
}

Hiding the Initial Setup

When the page first loads, there's a visible lag where columns with larger delays remain static until their animation starts. This can be avoided by using negative values for animation-delay. A negative delay starts the animation immediately but effectively pre-rolls it. The browser treats the animation as if it has already been running for the specified amount of time. This way, all columns are already mid-oscillation at page load, eliminating the awkward initial lag.

Styling the Corners and Fixing Gaps

Applying rounded corners to the whole flag can look messy because each column rounds independently. The fix is to use the :first-child and :last-child pseudo-classes to apply border-radius only to the outer edges of the first and last columns.

On some browsers, a thin gap can appear between columns due to pixel rounding on fractional widths. If the container's width and column count don't divide evenly (e.g., 200px / 12 columns = 16.666px), the rendering engine may show a 1-pixel seam. A reliable fix is to calculate a width that divides perfectly by the number of columns, ensuring each column has a integer pixel width.