This blog post includes moving SVGs! If you are sensitive to motion, please
do not read this article. The landing page, however, does respect your
prefers-reduced-motion
setting.
The animation will not work with Safari. To see how I fixed the issue on my landing page, you can see a my follow-up post.
When you visit my page, you are greeted with a nice, wavy background image. In this blog post I will let you know step by step how I created it so you can have it too :)
Everything starts with the cube
Before we can get into the details, we have to create our cube. Because the cube is viewed at an angle, we need three faces:
- top
- left
- right
<svg viewBox="0 0 20 20">
<!-- top face -->
<path d="M 0 5
L 10 0
L 20 5
L 10 10"
fill="hsl(46, 100%, 48%)" />
<!-- left face -->
<path d="M 0 5
L 0 15
L 10 20
L 10 10
Z"
fill="hsl(46, 100%, 45%)"/>
<!-- right face -->
<path d="M 10 10
L 20 5
L 20 15
L 10 20
Z"
fill="hsl(46, 100%, 39%)"/>
</svg>
In case you don’t know the syntax of SVG
<path>
s:
d
defines the shape of the path.M x y
tells the browser to move the “pen” to the coordinatesx
andy
.L x y
draws a line towardsx
andy
.Z
will close the path (aka, move the starting point).fill
will color the inside of the shape.
To better understand the result, I’ve split each face into it’s own SVG (use your dev tools if you want to inspect them):
Which is combined to:
You might have already guessed it, but the angle of the cube is defined by the “height” or “depth” of the top face:
But we are still missing the outline of our cube. First, you might think that we
can just add a
stroke
to
our faces, but then we get a weird artefact at the
corners:
That’s because each face’s edge will create its own corner. We can mitigate this
a bit by using
stroke-linejoin="round"
,
but it still looks a bit off, because
each corner does not respect that there are neighboring edges as well which
should be taken into consideration.
Instead, I added a separate “frame” for the outline:
<svg viewBox="0 0 20 20">
<!-- ...faces as before -->
<!-- outline -->
<g stroke="hsl(46, 100%, 35%)" stroke-linejoin="round" fill="none">
<!-- cage around the cube -->
<path d="M 0 5
L 10 0
L 20 5
L 20 15
L 10 20
L 0 15
Z
" />
<!-- inner edges -->
<path d="M 0 5 L 10 10" />
<path d="M 20 5 L 10 10" />
<path d="M 10 20 L 10 10" />
</g>
</svg>
And here is the output again:
Putting cubes next to each other
With the cube done, we have to put multiple cubes next to each other in order to
form a row. Instead of repeating the cube again and again, I chose to use the
<use>
SVG
element. It
allows you to reference another element to render it in-place. Sort-of like a
WebComponent. The nice part of this, is that the <use>
element can have its
own x
and y
coordinates and we don’t have to translate all the paths we have
crafted so far already.
Our first step is hence to wrap everthing into a <defs>
element:
<svg>
<defs>
<g id="q">
<!-- put the code we already have here -->
</g>
</defs>
</svg>
The nice thing about <defs>
is that it won’t be rendered. The included elements therefore won’t interfere with the actual rendering.
Next, we have to reference the new <g>
roup in the <use>
:
<svg>
<defs>
<g id="q">
<!-- ...the cube... -->
</g>
</defs>
<use href="#q" />
</svg>
That’s it. If you run this code, you won’t notice anything special:
However, we now have the capability to reference the cube multiple times:
<svg>
<!-- ... -->
<use href="#q" />
<use href="#q" />
</svg>
Hm, but if we run this, there is still only one cube. That’s because they are
both rendered on top of each other. But we want to display two cubes next to
each other now. To do that, we first need to increase the <svg>
’s viewBox
.
But to what value?
Our cube has a width of 20 (as defined by the path values). However, we have to add the width of the strokes as well. The default stroke width is 1. Halve the stroke width is drawn on one side of the line, the other half on the other. That way the path is always in the center of the stroke. That means in our current configuration, there is 0.5 stroke width on the left of the cube and 0.5 stroke width on the right of the cube for a total of 1. The total width of our cube is therefore 21.
If we want to draw two cubes next to each other, the canvas needs to be at least 42 wide.
<!--
I'm using -0.5 -0.5 for the min x and y values,
so that the stroke isn't cut off on the
left/top of the SVG.
-->
<svg viewBox="-0.5 -0.5 42 21">
<!-- unchanged for now -->
</svg>
Now we can offset the second <use>
by 21 on the x axis:
<svg>
<!-- ... -->
<use href="#q" />
<use href="#q" x="21" />
</svg>
And voilà:
The cubes are sticking next to each other now, which doesn’t look too nice. So
we just add 1 to the x
offset as well as the viewBox
’s width to create a gap
between the cubes:
<svg>
<!-- ... -->
<use href="#q" />
<use href="#q" x="22" />
</svg>
Putting cubes below each other
In my implementation, I needed to offset each other row so that they neatly fit in between each other. So the first question is, how far down should the second row be?
Because we can ignore the height of the cube’s body, the only relevant variable is the height of the top face. In the example I’ve been using so far, this value is 5. So let’s start by moving the first cube of the second row down by 5+0.5 (for the stroke width).
<svg>
<!-- ... -->
<use href="#q" />
<use href="#q" x="22" />
<use href="#q" y="5.5" />
</svg>
That looks like a mess, but could work. We have to move the cube to the right though. But by how much?
The cube needs to be centered between the two cubes. That means it needs to be
moved to the right by half a cubes total width - including the stroke width (=
21). But to center it between the cubes, we also have to add halve the gap
width (=1). So the total offset is (21 + 1) / 2 = 11
:
<svg>
<!-- ... -->
<use href="#q" />
<use href="#q" x="22" />
<use href="#q" y="5.5" x="11" />
</svg>
Perfect! However, the cubes are still sticking together too much, so we add
another gap. For the example below, I#ve added 1 to the y
coordinate.
<svg>
<!-- ... -->
<use href="#q" />
<use href="#q" x="22" />
<use href="#q" y="6.5" x="11" />
</svg>
To fill the row, we need to place another cube to the left and right of the
middle one. We already know the x-axis offset of those: +/-22
<svg>
<!-- ... -->
<use href="#q" />
<use href="#q" x="22" />
<use href="#q" y="6.5" x="-11" />
<use href="#q" y="6.5" x="11" />
<use href="#q" y="6.5" x="33" />
</svg>
Now I’m going to just fast-forward and add one row above and one row below to make the grid complete:
That completes the full background image.
The animation
The basic animation is very simple: Move each cube up by some amount and back down. Then repeat. However, to create a wave that travels across the screen, we have to use different offsets for different blocks. Those offsets are proportional to the distance to the wave’s center: The farther away the cube is from the wave origin, the longer it has to wait until it starts moving.
But let’s not get ahead of ourselves. First we need to add the animation. This
can be done by adding a <style>
element inside the <svg>
element. However,
the styles are not scoped to the SVG. They can still escape the SVG’s context.
For this example I will add class="cube"
to each <use>
.
<svg>
<!-- ... -->
<use class="cube" href="#q" y="-6.5" x="-11" />
<use class="cube" href="#q" y="-6.5" x="11" />
<use class="cube" href="#q" y="-6.5" x="33" />
<use class="cube" href="#q" />
<use class="cube" href="#q" x="22" />
<use class="cube" href="#q" y="6.5" x="-11" />
<use class="cube" href="#q" y="6.5" x="11" />
<use class="cube" href="#q" y="6.5" x="33" />
<use class="cube" href="#q" y="13" />
<use class="cube" href="#q" y="13" x="22" />
<style>
.cube {
animation: 3s wave ease-in-out infinite alternate;
}
@keyframes wave {
0% {
translate: 0 0;
}
100% {
translate: 0 -3px;
}
}
</style>
</svg>
As you can see in the example above, the cubes will now go up and down.
Now for the distance. The euclidean distance of two points on a 2D plane is
defined as sqrt((x1 - x2)² + (y1 - y2)²)
. Because we have a sqrt()
and a
pow()
function in CSS, we can do this calculation in pure CSS! All we need to
do is define the x and y coordinate of each cube via CSS variables.
What is the X and Y coordinate of the cube?
This question might sound trivial, because we are already setting the x
and
y
attribute. However, those define the top left corner of the bounding box
of the cube. That is not very accurate for our use-case. Instead, I have defined
the center of the cube as the center of the top face. I’ve marked it with a red
dot in the following SVG:
That means for each x
and y
attribute value we have to add halve the cubes
width (=10) and halve the cubes depth (=5) respectively. The SVG hence looks
like this after adding all the CSS variables:
<svg>
<!-- ... -->
<use class="cube" href="#q" y="-6.5" x="-11" style="--y: -1.5; --x: -1" />
<use class="cube" href="#q" y="-6.5" x="11" style="--y: -1.5; --x: 21" />
<use class="cube" href="#q" y="-6.5" x="33" style="--y: -1.5; --x: 43"/>
<use class="cube" href="#q" style="--y: 5; --x: 10" />
<use class="cube" href="#q" x="22" style="--y: 5; --x: 32" />
<use class="cube" href="#q" y="6.5" x="-11" style="--y: 11.5; --x: -1" />
<use class="cube" href="#q" y="6.5" x="11" style="--y: 11.5; --x: 21" />
<use class="cube" href="#q" y="6.5" x="33" style="--y: 11.5; --x: 43" />
<use class="cube" href="#q" y="13" style="--y: 18; --x: 10" />
<use class="cube" href="#q" y="13" x="22" style="--y: 18; --x: 32" />
<!-- ... -->
</svg>
Now the distance calculation. As already mentioned, we need to define the origin
of the wave first. You can pick whatever you want, I’m going with -3 -6
. I’ll
define this as --cx
and --cy
on the <svg>
element. Then I have everything
I need for my distance calculation.
<svg style="--cx: -3; --cy: -6">
<!-- ... -->
<style>
.cube {
--distance: sqrt( pow(var(--x) - var(--cx), 2) + pow(var(--y) - var(--cy), 2))
}
/* ... */
</style>
</svg>
Next we can use this distance to calculate the animation delay. Using the distance directly for the calculation could be an issue though. If we, for example, scale the SVG up by x2 (cube width is now 40 instead of 20), we don’t want the wave to be slower. If everything is scaled up, the wave should behave the same. So we have to normalize the values. On my website I’ve chosen to use the maximum of the canvas width and height to normalize the value. You can think of the distance value to then say “how much - in percent of the larger dimension
- is the cube away from the wave origin”.
In this example, I will do the same and divide the distance by the SVG width (=43), as it’s the larger dimension.
<svg style="--cx: -3; --cy: -6">
<!-- ... -->
<style>
.cube {
--distance: sqrt( pow(var(--x) - var(--cx), 2) + pow(var(--y) - var(--cy), 2));
animation-delay: calc(2s * var(--distance) / 43)
}
/* ... */
</style>
</svg>
The base value (2s
in my case) defines how fast the wave travels. Smaller
values mean that the wave becomes faster. Larger values make the wave slower.
Putting everything together, this is the result:
Closing thoughts
I hope that my blog post has inspired you to build your own SVG. There are still many things missing in my post. Most importantly: how to parameterize and auto-generate an SVG. Depending on your tech stack, this might be done using JavaScript or generated during compile time. So I leave this up for you to solve.
Personally, I’ve created the background image you are seeing on the home page using Hugo’s templating engine. That way I can focus on changing parameters around until I was happy.