DEV Community

tq-bit
tq-bit

Posted on • Updated on • Originally published at blog.q-bit.me

How to create svg elements with Javascript

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.

this image shows a comparison between the letter S being rendered with a raster format and the letter S being rendered with as an SVG

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>
Enter fullscreen mode Exit fullscreen mode

(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>
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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;
  }
}
Enter fullscreen mode Exit fullscreen mode

You'll receive a UI that looks like this, a simple and clean post collection.

this image shows a screenshot of a few posts that have been fetched from jsonplaceholder


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>
Enter fullscreen mode Exit fullscreen mode
  • 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');
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 image shows the final user interface that includes the svg-icons that serve as anchor tags

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

Top comments (6)

Collapse
 
brewinstallbuzzwords profile image
Adam Davis

Nice post! I did something pretty similar for one of my websites recently. Although instead of setting all the attributes in the js, I had them statically defined in my html template and bound certain values to angular variables. This allowed me to dynamically change the svg depending on user input.

The website is breadratiocalculator.com and it allows you to calculate bakers percentages and generate recipe cards (the recipe card preview is the dynamically updated svg)

Here's the github repo in case anyone would like to see the code

Collapse
 
tqbit profile image
tq-bit

Cool idea. Like that you can even conditionally change the whole svg's appearance or dynamically bind styling

Collapse
 
benracicot profile image
Ben Racicot

Great article! How did you (do we) get or calculate the path's d value?

Collapse
 
tqbit profile image
tq-bit • Edited

I took them from Heroicons(heroicons.com/). You can copy the d-attribute's value from the path tag.:

<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
  <path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
Enter fullscreen mode Exit fullscreen mode
Collapse
 
beniziranij profile image
Zainab

i did all of this it doesn't work

Collapse
 
tqbit profile image
tq-bit

Hi Zain. Can you please provide your code in a JSFiddle or Codepen example?

Just in case, I've put together the code pieces from above in one, in case you'd like to compare results:

jsfiddle.net/t9omepq8/6/