Building a wavy background in CSS

Week 283 was posted by Charanjit Chana on 2023-03-27.

Last week I saw a tweet from @jamesm and just had to see if I could recreate it in CSS.

My initial attempt was pretty poor in the end, although I could imagine radial gradients being the answer, I just got the approach all wrong. My excuse is it was late and I was genuinely trying to put something together as quickly as I could. I didn't spend more than 15 minutes on it.

While I'd approached the radial gradient themselves well enough, it hit me the next morning that instead of lots of steps, I actually needed to break it all down into lots of individual gradients.

Realised I completely messed up how I wanted to use radial gradients in the rush. Rather than a single radial gradients with lots of steps, stack lots of different gradients with different positioning! All achieved with just 1 element and CSS.

View on twitter

Not exactly the same as the original example, but the colours are tweak-able and for further sharing I decided to make the example orange to fit in more with my personal tastes.

As part of my write up, I reassessed how I'd approached the positioning which I had completely guessed and, for a reason I cannot remember, I went for negative values. And when I say I got it wrong, I had specified -1952% as one of the values.

Took a bit of guesswork before I could start to reign it in but settled on much more sensible numbers.

Explaining the CSS for the wavy background


So, let's break the CSS down. At the top, there are some custom properties to set various color values. This just makes theming easier for the future.

:root {
  /* color scale */
  --color-8: #ff5500;
  --color-7: #e64d00;
  --color-6: #cc4400;
  --color-5: #b33b00;
  --color-4: #993300;
  --color-3: #802b00;
  --color-2: #662200;
  --color-1: #4c1900;
  --color-0: #FF5500;

  /* color scale for box shadows */
  --color-transparent-3: #802b0055;
  --color-transparent-2: #66220055;
  --color-transparent-1: #4c190055;

I'd also set some properties on the body to help with centring the element and if I set the font-family just to prove text fit in the box.

Onto the div itself (some CSS omitted for brevity), which I'll break down line by line (or property by property).


This just sets the background color, should all else fail.

background-color: var(--color-6);


On to the background-image, with 4 separate radial gradients. As mentioned previously, breaking it down into 4 separate items, they can be positioned so that they can be offset against each other.

The gradients all have the same layout, just coloured differently for each step. They fade from a darker shade to a lighter one. The next step is two single pixel in an even lighter shade, which brings a nice and clear bit of separation between the layers. The last step is a fast fade to transparent which makes it the radial-gradient below visible.

    radial-gradient(var(--color-3), var(--color-4) 22%, var(--color-5) calc(22%), var(--color-5) calc(22% + 1px), transparent calc(22% + 3px)),
    radial-gradient(var(--color-4), var(--color-5) 24%, var(--color-6) calc(24%), var(--color-6) calc(24% + 1px), transparent calc(24% + 3px)),
    radial-gradient(var(--color-5), var(--color-6) 26%, var(--color-7) calc(26%), var(--color-7) calc(26% + 1px), transparent calc(26% + 3px)),
    radial-gradient(var(--color-6), var(--color-7) 28%, var(--color-8) calc(28%), var(--color-8) calc(28% + 1px), transparent calc(28% + 3px));

One of the reasons I didn't push it to 100% is that radial gradients can stretch beyond the box. I just found that this was the sweet spot during experimentation.


Next up, we set the background position of each of the radial-gradient. We just need to push the background down enough that it is towards the bottom of the element. Because we've also played with the background-size, and the pattern should be offset the value differs for the X and Y values. Since the background-size has been exaggerated quite a lot, the step for each gradient is actually quite small to achieve the effect.

background-position: 48% 32.5%, 46.5% 33%, 45% 33.5%, 43.5% 34%;


To achieve the effect, and given that the gradients don't stretch to 100% of their available space, the background size needs to be exaggerated a fair bit. For this example, I found the sweet spot was 2000%.

background-size: 2000% 2000%;

Would I use this in production? I'd have to weigh it up against an image of the same size. I wonder if I could compress a SVG enough to make it as lightweight as the CSS would be. Minified, I managed to get the div's CSS down to ~900 bytes.

I'd also look to play with the figures to make the steps larger, but the background-size and background-position values smaller which then fit the mental model of how it should all fit together.

Potential issues

I originally only set a single value for background-size but Chrome required two (even if they're the same) for it to stretch correctly. Chrome however, does a better of job of rendering the step that shows a line between each gradient. Safari makes it look fine, the effect works but it's definitely rendered better in Chrome.

Tags: css, development

Tweet WhatsApp Keep Upvote Digg Tumblr Pin Blogger LinkedIn Skype LiveJournal Like