# Waving Flag

Cut a flag into columns, stagger the motion, and it starts to look like cloth.

Published: 13th September 2026

This technique is taken from <a href="https://www.joshwcomeau.com/animation/pride-flags/" target="_blank" rel="noopener noreferrer">Josh Comeau</a>. I used it for the Indian flag in my footer.



Walk through the six steps. The playground is at the end.

## Step 1 : One image


Start with a real flag. I used the official Flag of India SVG from Wikimedia. Just an image. No motion. Looks correct. Feels dead.

```tsx showLineNumbers
<img src="/india-flag.svg" alt="" className="h-32 w-48" />
```

## Step 2 : Cut into columns


Pride flags can be painted with a linear gradient. I needed the Ashoka Chakra, so each column is a slice of the same image, shifted left.

```tsx showLineNumbers
{Array.from({ length: columns }, (_, index) => (
  <span key={index} className="waving-flag-column">
    <img
      src="/india-flag.svg"
      alt=""
      className="waving-flag-slice"
      style={{ left: `${-index * 100}%` }}
    />
  </span>
))}
```

Each column is a thin window with `overflow: hidden`. The image inside is `columns * 100%` wide.

- Column 0 shows the first strip (`left: 0%`)
- Column 1 is shifted `-100%`
- Column 23 is shifted `-2300%`

Same flag. Many thin windows.

## Step 3 : Oscillate


Every column uses the same keyframe. Ping-pong up and down. `ease-in-out` so it does not slam at the ends. `alternate` so it does not jump.

```css showLineNumbers
@keyframes oscillate {
  from {
    transform: translateY(var(--billow));
  }
  to {
    transform: translateY(calc(var(--billow) * -1));
  }
}

.column {
  animation: oscillate 500ms ease-in-out infinite alternate backwards;
}
```

Right now they all move together, same travel, same time. That is a trampoline, not a flag.

## Step 4 : Stagger


This is the whole illusion. Each column waits a little longer than the one on its left.

```tsx showLineNumbers
style={{
  animationDelay: index * staggeredDelay + 'ms',
}}
```

Set stagger to `0` and the wave dies. Add `50ms` back and the ripple returns.

Use a negative delay on the first column so the animation starts mid-cycle. No awkward second where the last strip is just sitting there.

```tsx showLineNumbers
const firstColumnDelay = columns * staggeredDelay * -1

animationDelay: firstColumnDelay + index * staggeredDelay + 'ms'
```

## Step 5 : Billow


Stagger is *when* a column moves. Billow is *how far*.

Both flags above use the same stagger (`50ms`). The left one gives every column the same travel. It waves, but it is a ribbon. The right one grows travel with the column index. That is a flag on a pole.

A real flag is tied on the left. The pole barely moves. The free edge flies.

```tsx showLineNumbers
style={{
  '--billow': index * billow + 'px',
}}
```

The keyframe never changes. Only `--billow` does.

```css showLineNumbers
@keyframes oscillate {
  from {
    transform: translateY(var(--billow));
  }
  to {
    transform: translateY(calc(var(--billow) * -1));
  }
}
```

With `billow = 0.8`:

- Column `0` travels `0px`. Pinned.
- Column `12` travels `9.6px`.
- Column `23` travels `18.4px`. The fly.

Without billow, every column uses one number, so pole and fly look the same. Set the playground knob to `0` and the shape dies. Put `0.8` back and the cloth returns.

## Step 6 : More slices


Eight fat columns look like blinds. Twenty four thin ones look like fabric.

Round the flag width so each column is a whole pixel. `200 / 12` is `16.666`. Firefox and Safari will show hairline gaps. `204 / 12` is `17`. No gaps.

```tsx showLineNumbers
const friendlyWidth = Math.round(width / columns) * columns
```

I keep the footer flag at `24px` by `16px` with `24` columns. One device pixel per strip. On hover it grows and the same wave reads as cloth.

## Playground

You now have the three knobs. Twist them. Break the wave on purpose.

- **Columns** toward 6: blinds. Toward 100: silk.
- **Stagger** at 0: trampoline again.
- **Billow** at 0: pole and fly look the same.


## Ready component

This is the version I use next to "Lucknow, India". Gradients swapped for the real flag image.

```tsx showLineNumbers
'use client'

import { type CSSProperties } from 'react'

const FLAG_COLUMNS = 24
const FLAG_BILLOW = 0.8
const FLAG_STAGGER = 50

export function WavingFlag() {
  const firstDelay = FLAG_COLUMNS * FLAG_STAGGER * -1

  return (
    <span
      className="waving-flag"
      style={{ '--columns': FLAG_COLUMNS } as CSSProperties}
      aria-hidden="true"
    >
      {Array.from({ length: FLAG_COLUMNS }, (_, index) => (
        <span
          key={index}
          className="waving-flag-column is-waving"
          style={
            {
              '--billow': `${index * FLAG_BILLOW}px`,
              animationDelay: `${firstDelay + index * FLAG_STAGGER}ms`,
            } as CSSProperties
          }
        >
          <img
            src="/india-flag.svg"
            alt=""
            className="waving-flag-slice"
            style={{ left: `${-index * 100}%` }}
          />
        </span>
      ))}
    </span>
  )
}
```

```css showLineNumbers
.waving-flag {
  display: inline-flex;
  width: 192px;
  height: 128px;
}

.waving-flag-column {
  position: relative;
  flex: 1 0 0;
  height: 100%;
  overflow: hidden;
}

.waving-flag-column.is-waving {
  animation: oscillate 500ms ease-in-out infinite alternate backwards;
}

.waving-flag-slice {
  position: absolute;
  top: 0;
  height: 100%;
  width: calc(var(--columns) * 100%);
  max-width: none;
}

@keyframes oscillate {
  from {
    transform: translateY(var(--billow));
  }
  to {
    transform: translateY(calc(var(--billow) * -1));
  }
}
```

Hence completed. Thank you for reading.

Original page: https://faisalhusa.in/craft/waving-flag
