SVG is a type of Extensible Markup Language (XML) - it uses tags to format or âmark upâ its content.
The bare minimum required for a standalone SVG file are some opening and closing <svg>
tags.
The opening tag must containing the XML namespace declaration, which tells anyone who is interested that this is an SVG file, and exactly which definition of âSVGâ you mean by that.
<svg xmlns="http://www.w3.org/2000/svg">
<!-- âď¸ your drawing here đ -->
</svg>
Note: the SVG element has an opening <svg>
tag and a closing </svg>
tag (â see the backslash?) This is so that we can put other elements within the SVG element.
Save this file, and give it a .svg extension, eg. âCool-Drawing.svgâ. Wow, cool drawing.
But this is just an empty SVG file so far. If you open it in the browser, youâre going to be disappointed. You still need to draw your image within these <svg>
tags.
This is the simplest SVG drawing that I think it is possible to make. It has a tiny wee filesize of just 63 bytes.
Letâs draw a <circle>
element with a radius of 4 units: r="4"
<svg xmlns="http://www.w3.org/2000/svg">
<circle r="4"/>
</svg>
We will not be putting any other elements within our circle element, so we can make it a single âself-closingâ tag, with a backslash at the end, like so: <circle/>
If you open this file in the browser, youâll see that itâs not just the filesize thatâs small:
Whyâs the circle so tiny and up there on the left? and whereâs the rest of it?
Letâs get a closer look at it, by making our SVG fill 100%
of the browserâs height:
<svg xmlns="http://www.w3.org/2000/svg" height="100%">
<circle r="4"/>
</svg>
Huh, that didnât help at all.
Alright, itâs time to add the most marvellous and powerful SVG attribute, the one that puts the âscalableâ into vector graphics, the mighty viewBox
!
An SVG file is a partial view of the potentially infinite canvas that you can think of as âSVG Worldâ. When we told the browser to fill the screen (without any further instructions), it put as much of SVG World into view as it could, which may have been a few hundred or a couple of thousand pixels worth (one pixel per SVG unit). So our circle only made up 4 pixels, a tiny fraction of that.
If we just want to view a particular section of SVG World (trust me, we do) then we can use a viewBox
to do so. Itâs an attribute that you should spend a bit of time with and get to know, because itâs your camera into SVG World - and just like a camera, you can move it around, pan and zoom wherever you like. A viewBox
can even crop your image to different aspect ratios.
OK, letâs determine the size and shape of the box through which we view our SVG, and the specific part of Infinite SVG World that we want to look at. The viewBox has four values:
x
value of the top left cornery
value of the top left cornerwidth
of the viewBoxheight
of the viewBox
0,0
seems an appropriate place for our image to start, and letâs make the size of our image 20 units across, and 20 units down.
Add a viewBox
property to the opening <svg>
tag with the value "0 0 20 20"
:
<svg
xmlns="http://www.w3.org/2000/svg"
height="100%"
viewBox="0 0 20 20"
>
Finally! Now we can start to see whatâs going on.
OK, Iâm going to lend you a bunch of SVG code to help us see this more clearly. Donât worry about understanding this code for now - it adds a grid of dots and labelled x & y axes to your drawing. Weâll just use it temporarily as a visual guide to help you get used to the SVG coordinate system, and delete it later. Paste it into your SVG straight after the opening <svg>
tag, and before your <circle>
.
<!-- SVG Coordinates Guide -->
<defs><pattern id="dots" x="0" y="0" width="0.05" height="0.05">
<circle r="0.05" cx="0.5" cy="0.5" fill="#ccc"/></pattern></defs>
<path d="M0,0h20v20h-20z" fill="#eee"/>
<path d="M0.5,0.5h20v20h-20z" fill="url('#dots')"/>
<path stroke="blue" stroke-opacity="0.5" stroke-width="0.05" fill="none"
d="M-1,0h6v-0.5m0,0.5h5v-0.5m0,0.5h5v-0.5m0,0.5h5v-0.5
M0,-1v6h-0.5m0.5,0v5h-0.5m0.5,0v5h-0.5m0.5,0v5h-0.5"/>
<g font-family="monospace" font-size="0.5px" fill="gray">
<g text-anchor="middle">
<text x="5" y="-1">5</text><text x="10" y="-1">10</text>
<text x="15" y="-1">15</text><text x="20" y="-1">20</text></g>
<text y="5.1" x="-1.5">5</text><text y="10.1" x="-1.5">10</text>
<text y="15.1" x="-1.5">15</text><text y="20.15" x="-1.5">20</text></g>
<!-- âŹď¸ Your drawing starts here âŹď¸ -->
The other thing that we can do to understand what the viewBox did for us, is that we can style
our SVG with a 1px red border
in order to reveal the viewBox:
<svg
xmlns="http://www.w3.org/2000/svg"
height="100%"
viewBox="0 0 20 20"
style="border: 1px red solid;"
>
Now you should see the dot grid, and a red outline around your SVG. Letâs change the viewBox values to get a better view. Weâre essentially âzooming outâ by 20% here, so that the top left corner starts at -2,-2
and weâre now viewing a 24Ă24 unit section of SVG World.
<svg
xmlns="http://www.w3.org/2000/svg"
height="100%"
viewBox="-2 -2 24 24"
style="border: 1px red solid;"
>
Now we can see the labelled axes, you will see that the top left corner of the viewBox is at -2,-2
and that it is 24
units wide and high, i.e. it stretches from -2
to 22
on both axes. Our ÄĽeight
attribute that we set earlier ensures that those 24 units are scaled to fit the height of the screen.
Play around with the viewBox values to see what they do, checking the result in the browser:
Make it wide. Make it tall and skinny. Try to make the viewBox show only the bottom right quarter of the grid. Zoom in. Zoom out.
Got the hang of the viewBox now? âŚkinda? good enough, onwards!
p.s. donât forget to set your viewBox back to â-2 -2 24 24â
when youâre done experimenting.
Although we told the browser to draw a circle with a radius of 4, we didnât tell it where we wanted it, or what color it should be. So it did what it thought was most helpful, and drew it centred on 0,0
with a black fill. Anytime that you donât specify positions or colors, the browser (who has some weird gothy OCD tendencies) will paint things black and stack them carefully in the corner.
So letâs move this circle elsewhere and give it some color. A <circle>
element is positioned with a âcenter Xâ (cx
) and âcenter Yâ (cy
) value, which⌠determine where its center will be.
This circle is going to be our sun, so letâs make its fill
color LightYellow
.
<circle r="4" cx="5" cy="5" fill="LightYellow" />
Now weâre getting somewhere! Letâs draw our picture!
In order to explore a couple more SVG elements, weâre going to draw a house. Letâs start with the main structure, which will be a rect
(rectangle) element. The x
and y
value determine the position of the top left corner, and Iâm going to let you figure out what width
and height
do.
<rect x="11" y="10" width="5" height="4" fill="Peachpuff"/>
Our house needs a roof, so weâll use a three-sided polygon
- but itâs actually more useful to think of it as a three-pointed polygon. We want our roof to start just to the left of our house, at 10,10
, then go up to a central peak at 13.5,7
, then it would be nice and symmetrical if the third point was 17,10
. So weâll use those coordinates as the points
for our polygon, and the triangular shape between those three points will be filled with a delicious Tomato
color. Your roof doesnât have to be triangular, by the way. A polygon can be as many points as you like, just keep chucking more in there, it will be fine. Build a full-blown McMansion roof if youâre feeling extravagant. Iâll stick with my little tomato triangle though.
<polygon points="10,10 13.5,7 17,10" fill="Tomato"/>
Note: the colors Iâm using are chosen from the list of Named HTML Colors, which is a pretty haphazard collection of unevenly distributed colors with sometimes terrible names. Many were chosen and named by software developers in the '80s holding up crayons next to the screen. Some may have been chosen by monkeys flinging poop. They donât make much sense from any perspective of color science or artistic color theory. But itâs a lot easier to remember the name Peachpuff
than ffdab9
or rgb(255, 218, 185)
so theyâre useful when youâre quickly âsketchingâ with code like this.
Now weâve got three shapes in our image, and theyâre all different elements, so itâs pretty easy to guess which shape in the code corresponds to which shape in the drawing. But as we add more shapes, this is going to get a lot more confusing. Try to be kind to anybody who might read your code (yourself included) and add an id
to each shape. That way you can quickly scan your file and find the part youâre looking for. An id
will also come in handy later, when we start animating.
<circle id="sun" r="4" cx="5" cy="5" fill="LightYellow" />
<rect id="housefront" x="11" y="10" width="5" height="4" fill="Peachpuff"/>
<polygon id="roof" points="10,10 13.5,7 17,10" fill="Tomato"/>
Letâs pile on the last few shapes for our house - a door, a window, and a chimney.
<circle id="sun" r="4" cx="5" cy="5" fill="LightYellow" />
<rect id="housefront" x="11" y="10" width="5" height="4" fill="Peachpuff"/>
<polygon id="roof" points="10,10 13.5,7 17,10" fill="Tomato"/>
<rect id="door" width="1" height="2.5" x="12" y="11.5" fill="SteelBlue"/>
<rect id="window" width="1" height="1" x="14" y="11" fill="SteelBlue"/>
<rect id="chimney" width="1" height="2" x="15" y="8" fill="Chocolate"/>
If you look at our image now, you might see that thereâs a problem. The chimney sits in front of the roof, but it would look a lot tidier if it were behind the roof. Itâs good to remember that when a browser reads SVG, it will draw elements in the order it reads them. So elements written earlier in the file will be drawn first. Anything that comes after will be drawn on top of them.
Note: This is a bit counter-intuitive for people used to using desktop graphics applications like Krita, Glimpse or Photoshop, where layers that are higher are closer to the viewer. But youâve already seen that the coordinates in SVG go downwards too, so youâll just have to get used to everything being upside down đ in SVG World.
Where were we? ah, yes, the chimney. Cut it from its current position, and paste it directly before the roof in the code. That will put it behind the roof in the image:
<circle id="sun" r="4" cx="5" cy="5" fill="LightYellow" />
<rect id="housefront" x="11" y="10" width="5" height="4" fill="Peachpuff"/>
<rect id="chimney" width="1" height="2" x="15" y="8" fill="Chocolate"/>
<polygon id="roof" points="10,10 13.5,7 17,10" fill="Tomato"/>
<rect id="door" width="1" height="2.5" x="12" y="11.5" fill="SteelBlue"/>
<rect id="window" width="1" height="1" x="14" y="11" fill="SteelBlue"/>
Our house is now getting more complicated, it is made of of 5 shapes now. Which is unfortunate, as, for complex artistic reasons known only to me, I have decided that it needs to be a bit bigger, and in a slightly different place. Which means weâre going to have to change 4 values in each <rect>
, and the three sets of coordinates in the <polygon>
, and do a bunch of annoying maths in our heads.
If only there was a better way!
Of course there is, you silly sausage. Weâll use a group! We can wrap all those house elements within <g></g>
tags, and then move or scale the group! We can apply a transform
to both translate
and scale
the group.
<g id="house" transform="scale(1.2) translate(-2,-1)">
<rect id="housefront" x="11" y="10" width="5" height="4" fill="Peachpuff"/>
<rect id="chimney" width="1" height="2" x="15" y="8" fill="Chocolate"/>
<polygon id="roof" points="10,10 13.5,7 17,10" fill="Tomato"/>
<rect id="door" width="1" height="2.5" x="12" y="11.5" fill="SteelBlue"/>
<rect id="window" width="1" height="1" x="14" y="11" fill="SteelBlue"/>
</g>
Now we can just add some grass and some sky, trim the viewBox to the actual size of our image, and delete the Coordinates Guide (you donât need it anymore, Iâm so proud of you đď¸đ˘ď¸)
âŚand⌠our image is done!
<svg xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 20 20" >
<rect id="sky" width="20" height="20" fill="SkyBlue"/>
<circle id="sun" r="4" cx="5" cy="5" fill="LightYellow" />
<rect id="grass" width="20" height="7" y="13" fill="MediumSeaGreen"/>
<g id="house" transform="scale(1.2) translate(-2,-1)">
<rect id="housefront" x="11" y="10" width="5" height="4" fill="Peachpuff"/>
<rect id="chimney" width="1" height="2" x="15" y="8" fill="Chocolate"/>
<polygon id="roof" points="10,10 13.5,7 17,10" fill="Tomato"/>
<rect id="door" width="1" height="2.5" x="12" y="11.5" fill="SteelBlue"/>
<rect id="window" width="1" height="1" x="14" y="11" fill="SteelBlue"/>
</g>
</svg>
So far we have been adding attributes inline, that is, directly writing the fill color and the transforms and all the rest within an elementâs tag.
When it comes to adding animation to our SVG however, thereâs a whole lot more extra information to include - things would definitely get messy if we were to keep everything inline. So weâll keep our animation code confined to a <style>
section. This can be placed anywhere in an SVG file, but the convention is to put it at the start of the file, before our drawing.
<svg xmlns="http://www.w3.org/2000/svg" height="100%" viewBox="0 0 20 20" >
<style>
/* đ´đťâđ¨ď¸ Animation details go here đ */
/* anything within these style tags is written in CSS syntax, not SVG/XML */
</style>
<rect id="sky" width="20" height="20" fill="SkyBlue"/>
<circle id="sun" r="4" cx="5" cy="5" fill="LightYellow" />
...
So far we have been writing SVG, but now weâre going to switch it up and write some CSS inside our SVG. But as youâll see itâs not that different really. Letâs use CSS to move the sun in our image downwards, so its center is below the horizon.
We use #sun
as the selector, which means âthe element with an id of sunâ. That goes at the start of a new line, and then we open some curly braces, in which we will declare the specific styling we want to apply to #sun
. In our case, we want a translate
transform
, which operates just like the SVG transform we wrote earlier, it just has slightly different punctuation.
<style>
#sun {
transform: translate(0, 10px);
}
</style>
How come I wrote âpxâ after 10?
So far weâve been getting along fine without any units, why ruin a good thing now?
Well, SVGâs a bit more relaxed about units than most programming languages. Itâs all, like âjust create, man, just feel it - donât let the metric system get you down, follow your own coordinate system, you know? deal with display sizes laterâŚâ
But CSSâ whole existence is based on rules. CSS is sick of tidying up SVGâs shit, and they really need SVG to just grow up and pick an explicit unit for once in their life.
So when SVG is talking to CSS it pretends that itâs following the nice, regimented, px-based system that CSS likes, and accepts rules like âtranslate 10pxâ - âoh, sure thing, bossâŚâ but the SVG is actually following its own coordinate system internally, because itâs not going to let the Man get in its head.
Thanks to the 20Ă20 viewBox and the height
of 100%
, in this case, CSSâ 10px
doesnât mean 10 pixels on the screen, it means half the height of the image, and the image might be displayed as ~1000px high, depending on the screen itâs viewed on. If the sun element was sitting in a group with a scale
transform
, that would mess with the number of actual screen pixels translated even further.
So the moral of the story is, when CSS says âmove the sun by 10pxâ to an SVG, it doesnât mean 10 pixels on the screen. It means 10 SVG units in whatever context and coordinate system the object might be. âItâs all, like, relative, man.â
How come I didnât write âpxâ after 0?
Well, zero is CSSâ happy place. Zero is Zero is Zero. Nobody ever breaks that rule.
Zero pixels = Zero nautical miles = Zero feet = Zero nanometers = Zero banana emojis. Everything is just so clear, consistent and unambiguous. All is well with the world. This is the only time that CSS really just relaxes and lets its hair down. What?! You want to write a distance without units? HOW DARE Yâ Wait, is it... zero?
Oh, well thatâs no problem at all, friend, just go right ahead.
I hope you enjoyed that in-depth psychological analysis of the the fraught relationship between CSS and SVG, but I realise that you may have other concerns, like âwhy doesnât this picture move? I was promised animation, instead the sun is just in a different place. What is this shit?â
Ok, weâll make it move then. jeez.
Hang on to that transform: translate(0, 10px);
declaration, weâre going to use it in the animation soon.
But first weâll need a name for our animation: sunset
seems appropriate. Letâs also give it a duration of 4 seconds.
#sun {
animation-name: sunset;
animation-duration: 4s;
}
But sunset
doesnât mean anything. Yet. We need to declare what actually happens. Below the #sun
ruleset weâll write some @keyframes
which will determine what properties change during the animation, how much and at what point they change. As weâre just going from one position to another, we only need two keyframes: from
and to
:
#sun {
animation-name: sunset;
animation-duration: 4s;
}
@keyframes sunset {
from {
transform: translate(0, 0);
}
to {
transform: translate(0, 10px);
}
}
Wa-hey! itâs animated! But it only runs once, in one direction, and how come itâs slow at the start and end?
Letâs deal with these one at a time. You can set the number of times an animation will play using animation-iteration-count
: a value of 3
will play 3 times, 1.5
will stop halfway through the second run, etc. We want this sun to be rising and setting, back and forth forever, so weâll loop it with a value of infinite
.
#sun {
animation-name: sunset;
animation-duration: 4s;
animation-iteration-count: infinite;
}
We donât just want our sun to set and then suddenly jump back into position at the end of the animation - instead we want the sun to smoothly return once it has set. To do this, we can set animation-direction
to alternate
.
#sun {
animation-name: sunset;
animation-duration: 4s;
animation-iteration-count: infinite;
animation-direction: alternate;
}
Finally, letâs have a look at animation-timing-function
or whatâs often referred to as âeasingâ or âspacingâ.
In the physical world, if you were to move an object with perfectly even or âlinearâ timing, maintaining a constant speed from the first millisecond to the last, it would look and feel very unnatural, almost robotic. It would also be basically impossible, because in the physical world, things have mass and momentum, energy and friction.
If you flick a ping-pong ball, its acceleration will seem quite instantaneous (itâs not) whereas if you try to push a boulder, it will take a few seconds of sweating and grunting before it picks up any speed.
So although a linear
timing function is the most mathematically simple, most of the time animators tend to use some âeasingâ to give a more natural sense of movement. If something is shooting in from off-screen and coming to a stop, you would choose an ease-out
timing function, so that the end of the animation (the âoutâ) is gradually slowed (eased). If something is exiting the screen, you may prefer an ease-in
function, which means the start (in) of the animation is slower, gradually picking up speed and then maintaining constant speed near the end, to indicate that it will continue moving once off-screen.
The default timing function in CSS is something called ease
which should look kinda okay for most animations, I guess. It has a slight ease-in combined with a long ease-out, and thatâs what our sunset
animation is showing right now.
For our animation, however, out animation will play forwards and reversed, over and over again, so we want the timing function to be symmetrical - weâll use ease-in-out
which combines a slow start with an equivalent slow end.
#sun {
animation-name: sunset;
animation-duration: 4s;
animation-iteration-count: infinite;
animation-direction: alternate;
animation-timing-function: ease-in-out;
}
Note: Most of the time I use the animation
shorthand instead of writing out all of these properties long-form. You can just chuck all of these values in row like this - as long as youâre not using a delay
, the order doesnât matter at all:
#sun { animation: sunset 4s infinite alternate ease-in-out; }
OK, the sun sets and returns! But it would be great if the light changed in our scene when the sun drops behind the horizon. letâs addâŚ
When the sun sets, weâre going to make the sky pinker, and the overall scene dark-bluer. Weâre going to need two new rect
elements:
<rect id="sky" width="20" height="20" fill="SkyBlue"/>
<!-- the Pink rect will appear over the sky -->
<rect width="20" height="20" fill="Pink"/>
<circle id="sun" r="4" cx="5" cy="5" fill="LightYellow" />
<rect id="grass" width="20" height="7" y="13" fill="MediumSeaGreen"/>
<g id="house" transform="scale(1.2) translate(-2,-1)">
<rect id="housefront" x="11" y="10" width="5" height="4" fill="Peachpuff"/>
<rect id="chimney" width="1" height="2" x="15" y="8" fill="Chocolate"/>
<polygon id="roof" points="10,10 13.5,7 17,10" fill="Tomato"/>
<rect id="door" width="1" height="2.5" x="12" y="11.5" fill="SteelBlue"/>
<rect id="window" width="1" height="1" x="14" y="11" fill="SteelBlue"/>
</g>
<!-- the MidnightBlue rect will appear over the whole image -->
<rect width="20" height="20" fill="MidnightBlue"/>
Well now we canât see anything. What we need to do is to change the blending mode of our MidnightBlue rectangle - you may or may not be familiar with the concept of blending modes from desktop graphics applications, but itâs a way of determining how one element will mix, cover, affect or intersect with the elements below it. In our case, we want the hue
of the shapes in our image to become more MidnightBlue. There are native blend modes in SVG but theyâre kind of complicated, so the CSS developers reworked them in CSS, making them much simpler to write and understand. So weâre going to use the CSS syntax inline here, by putting the ruleset in a style
attribute:
<rect width="20" height="20" fill="MidnightBlue" style="mix-blend-mode: hue;">
Now the scene looks suitably dusky.
We want to animate both our Pink
and MidnightBlue
rectangles at the same time, in the same way, so rather than address them by id
, as we have been doing, Iâm instead going to add the same class
to both elements.
<rect class="dark" width="20" height="20" fill="Pink"/>
...
<rect class="dark" width="20" height="20" fill="MidnightBlue"
style="mix-blend-mode: hue;"/>
Then we can easily write CSS rulesets that apply to multiple elements, even if theyâre different types of element. In the <style>
section, below our sunset
keyframes, letâs add a new ruleset, selecting the âdarkâ class with the . symbol. I want this to be in sync with the sun, so Iâll keep everything but the
animation-name
consistent:
.dark { animation: darken 4s infinite alternate ease-in-out; }
Weâll also define the darken
animation using @keyframes
:
@keyframes darken {
from {
opacity: 0;
}
to {
opacity: 0.9;
}
}
Note: You can also use 0%
and 100%
instead of from
and to
- for animations that are more complex than simply transitioning from one state to another, you may want to also add in another keyframe at 40%
, and another at 60%
, etc.
Finished! We now have a beautiful sunset image, which loops forever, and ever, and everâŚ
You can now use this in a website! For maximum scalabily and flexibility, I would recommend removing the height
attribute we included in the <svg>
tag and instead setting the size of the image in the HTML/CSS, so that you can adjust it based on your layout.
There are a few ways that you can include the SVG, eg. by including it in an <img>
tag in your HTML, like so:
<!DOCTYPE html>
<html>
<head>
<style>
img {
width: 60vw;
margin: auto;
}
</style>
</head>
<body>
<h1>Woah, Check Out This Sweet Animation</h1>
<img
alt="animation of a sunset behind a little house"
src="../images/My-Cool-Drawing.svg"
>
</body>
</html>
Or, you could include the SVG inline in the HTML, which is what I would recommend if you want to do anything interactive and javascripty with it:
<!DOCTYPE html>
<html>
<head>
<style>
svg {
width: 60vw;
margin: auto;
}
</style>
</head>
<body>
<h1>Woah, Check Out This Sweet Animation</h1>
<svg viewBox="0 0 20 20" >
<title>My Cool Drawing</title>
<desc>animation of a sunset behind a little house</desc>
<style>
#sun { animation: sunset 4s infinite alternate ease-in-out; }
@keyframes sunset {
from { transform: translate(0, 0); }
to { transform: translate(0, 10px); }
}
.dark { animation: darken 4s infinite alternate ease-in-out; }
@keyframes darken {
from { opacity: 0; }
to { opacity: 1; }
}
</style>
<rect id="sky" width="20" height="20" fill="SkyBlue"/>
<rect class="dark" width="20" height="20" fill="Pink"/>
<circle id="sun" r="4" cx="5" cy="5" fill="LightYellow" />
<rect id="grass" width="20" height="7" y="13" fill="MediumSeaGreen"/>
<g id="house" transform="scale(1.2) translate(-2,-1)">
<rect id="housefront" x="11" y="10" width="5" height="4" fill="Peachpuff"/>
<rect id="chimney" width="1" height="2" x="15" y="8" fill="Chocolate"/>
<polygon id="roof" points="10,10 13.5,7 17,10" fill="Tomato"/>
<rect id="door" width="1" height="2.5" x="12" y="11.5" fill="SteelBlue"/>
<rect id="window" width="1" height="1" x="14" y="11" fill="SteelBlue"/>
</g>
<rect class="dark" width="20" height="20" fill="MidnightBlue"
style="mix-blend-mode: hue;"/>
</svg>
</body>
</html>
Notes:
- If youâre including your SVG inline in HTML, you donât need the
xmlns="http://www.w3.org/2000/svg"
namespace declaration. - SVG gives you much more flexibility with scaling compared to other image formats. But that flexibility can get complicated. Amelia Bellamy-Royds is the #1 expert on SVG, and she's got an overview article to help you out: How to Scale SVG.