In a nutshell: What are SVGs?
If you've ever taken a small image and tried to scale it up in size, you know the struggle: It gets pixelated and the fonts become an unreadable raster of black-to-white'ish squares. Fortunately, there are resolutions to the matter, one of which has been standardized within the .svg
file format. While other common formats, such as .png
, are based on a grid of pixels, svgs consist out of a fixed set of shapes. The way these are drawn and aligned is described with XML - markup, more specifically with paths. This allows for a more dynamic scaling.
Yug, modifications by 3247, CC BY-SA 2.5, via Wikimedia Commons
In a nutshell, raw SVG files in the wilderness:
- are namespaced within their xml namespace (xmlns) - standard.
- contain one or several paths within the - tags that make up the actual graphc.
- can be styled with css and inline styles.
Consider this example from Heroicons. If you drop the markup into an html file, it will render into the actual icon.
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
(note: I replaced the tailwind classes with a style attribute, the result is about the same though)
Now that you've got a glimpse about the format, you might already have an idea how the post's topic is to be solved - by means of DOM - manipulation. So let's try and recreate the element above with Javascript.
Dynamic XML-node creation - boilerplate
XML differs from HTML in several aspects, the most relevant being that XML does not have predefined tags. Instead, it allows you to define these yourself within so-called namespaces.
This also allows for dynamically adding SVG icons to data from a remote location you'd like to bind to a client's interface while - or after - the data is being rendered. Let's assume you run a blog and would like to dynamically add the 'link'-icon from above before every post's heading. For a user's convenience, we'll add an anchor tag that permits the reader to scroll this post directly scroll it into their center of attention. To illustrate this example, let's start with the following boilerplate:
- We use a simple
index.html
file that holds a list of posts. - These posts are fetched from jsonplaceholder and dynamically added to the DOM by a function inside the
main.js
file. -
main.css
provides us a few basic styles for our list.
So launch your favorite text editor and add them to a free directory of your choice.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="main.css">
<title>Create SVGs with Javascript - Demo</title>
</head>
<body>
<h1 class="site-header">
Posts from today
</h1>
<main id="posts" class="post-list"></main>
<script src="main.js"></script>
</body>
</html>
main.js
async function getPostData() {
const url = 'https://jsonplaceholder.typicode.com/posts';
const response = await fetch(url);
return await response.json();
}
function renderPosts(app, posts) {
const postNodes = posts.map((post) => {
// Create the DOM elements
const postCard = document.createElement('div');
const postHeader = document.createElement('div');
const postTitleAnchor = document.createElement('a');
const postTitle = document.createElement('h2');
const postText = document.createElement('p');
// Add some classes and attributes
postCard.classList.add('post-card');
postHeader.classList.add('post-header');
postTitle.classList.add('post-title')
postTitle.id = post.title;
postTitleAnchor.href = '#' + post.title;
// Place the text content
postTitle.textContent = post.title;
postText.textContent = post.body;
// TODO: Add the icon here
// Put together the DOM nodes
postHeader.appendChild(postTitleAnchor)
postHeader.appendChild(postTitle);
postCard.appendChild(postHeader);
postCard.appendChild(postText);
app.appendChild(postCard);
return postCard;
});
return postNodes;
}
async function mountPosts() {
const app = document.querySelector('#posts');
const posts = await getPostData();
renderPosts(app, posts);
}
mountPosts();
main.css
* {
scroll-behavior: smooth;
}
body {
font-family: 'Gill Sans', 'Gill Sans MT', Calibri, 'Trebuchet MS', sans-serif;
background-color: blueviolet;
margin: 0;
padding: 0;
}
h1 {
padding: 2rem 0;
margin: 0;
}
.site-header {
position: sticky;
text-align: center;
width: 100%;
background-color: #fff;
}
.post-list {
padding: 0 20vw;
}
.post-card {
border-radius: 2rem;
background-color: #fff;
padding: 1rem 2rem;
margin: 2rem;
}
.post-icon {
transition: 0.25s all;
border-radius: 0.25rem;
height: 2rem;
width: 2rem;
margin-right: 0.5rem;
padding: 0.25rem;
}
.post-icon:hover {
transition: 0.5s all;
background-color: blueviolet;
stroke: white;
}
.post-header {
display: flex;
align-items: center;
}
@media only screen and (max-width: 1200px) {
.post-list {
padding: 0 10vw;
}
}
@media only screen and (max-width: 600px) {
.post-list {
padding: 0 2vw;
}
}
You'll receive a UI that looks like this, a simple and clean post collection.
Add a function to create the XML
Let's take a look into the xml-file again:
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1" />
</svg>
- It has a tag as a wrapper which includes the namespace and some attributes.
- Within, there's one (or several) tags that describes the shape of the SVG.
- Inside the browser's context, both of these are interpreted and rendered like html-tags.
The last point also implies that said xml-tags can be created and composed like html-elements. An tag, for instance, can be created like this:
// Create an element within the svg - namespace (NS)
document.createElementNS('http://www.w3.org/2000/svg', 'svg');
From then on, the svg can be mostly be handled like any other element. You can add styles, classes and also - most importantly - attributes.
So let's add the following function to the main.js
file. It will take in the anchor tag into which we will inject the created graphic, making it suitable for our scrolling feature.
function renderLinkIcon(node) {
const iconSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
const iconPath = document.createElementNS(
'http://www.w3.org/2000/svg',
'path'
);
iconSvg.setAttribute('fill', 'none');
iconSvg.setAttribute('viewBox', '0 0 24 24');
iconSvg.setAttribute('stroke', 'black');
iconSvg.classList.add('post-icon');
iconPath.setAttribute(
'd',
'M13.828 10.172a4 4 0 00-5.656 0l-4 4a4 4 0 105.656 5.656l1.102-1.101m-.758-4.899a4 4 0 005.656 0l4-4a4 4 0 00-5.656-5.656l-1.1 1.1'
);
iconPath.setAttribute('stroke-linecap', 'round');
iconPath.setAttribute('stroke-linejoin', 'round');
iconPath.setAttribute('stroke-width', '2');
iconSvg.appendChild(iconPath);
return node.appendChild(iconSvg);
}
Making it all functional
Now that we have all building blocks in place that adds the icon, let's put it to action.
Add the following inside the main.js
file, right after placing the text-content:
// TODO: Add the icon function here
renderLinkIcon(postTitleAnchor);
And that's it. The icons are prepended to each post and can easily be used as anchor links for smooth scrolling. Below goes the final result:
This post was originally published at https://blog.q-bit.me/how-to-create-svg-elements-with-javascript/
Thank you for reading. If you enjoyed this article, let's stay in touch on Twitter 🐤 @qbitme