Jxnblk

Building SVG Icons with React

How to Create Mathematically-Generated Graphics Using JavaScript and React

View DemoGitHub

While traditional graphics applications like Adobe Illustrator work well for certain tasks, they fall short when used to create pixel perfect, mathematically-derived graphics. Anyone who’s attempted to create data visualizations with such software might have encountered these limitations. And while JavaScript libraries like D3 have helped out tremendously, certain types of illustrations and icons can still be difficult to create, and tools like Illustrator leave a lot of room for error.

Take a settings cog icon as an example. It relies on radial symmetry and is based on three concentric circles, where lines from each tooth must intersect at points not easily determined within a two dimensional grid. Creating something like this in graphics software would require using transformations every time an adjustment was made. Luckily, some basic math can help out, and JavaScript excels at making these sorts of calculations.

Although most of this could be achieved with plain JavaScript and other templating engines, using a library like React provides high cohesion between the SVG code and the math involved, keeps things encapsulated into a single requirable component, and provides an easy way to render static markup.

Note: this tutorial focuses on using React as a stand-alone exploratory design tool, rather than as a way to implement SVG icons within a React application.

Making a Cog Icon

Cog icon sketch
Icon sketch made using a salt shaker, Sixpoint can, and a quarter

Looking at this sketch, there are some properties that can be handled with JavaScript variables:

Initial Setup

To get started, you should have Node.js installed as well as a basic understanding of using npm and JavaScript modules. Initialize a new npm package and install react and react-tools

npm init
npm i --save-dev react react-tools

Now create a build.js script which will be used to render the static SVG.

var fs = require('fs')
var React = require('react')
var Cog = require('./Cog')

var build = function(name, props) {

  var svg = React.renderToStaticMarkup(React.createElement(Cog, props))
  
  fs.writeFileSync('icons/' + name + '.svg', svg)

}

build('cog-icon', {})

This build script imports the Cog component, renders it to static markup, and saves the file in the icons directory.

Go ahead and make a new directory for the icons.

mkdir icons

Next create a src folder and a new Cog.jsx file.

var React = require('react')

var Cog = React.createClass({

  getDefaultProps: function() {
    return {
      size: 64,
      fill: 'currentcolor'
    }
  },

  render: function() {

    var size = this.props.size
    var fill = this.props.fill

    var viewBox = [0, 0, size, size].join(' ')

    var pathData = [
      ''
    ].join(' ')

    return (
      <svg xmlns="http://www.w3.org/svg/2000"
        viewBox={viewBox}
        width={size}
        height={size}
        fill={fill}>
        <path d={pathData} />
      </svg>
    )

  }

});

module.exports = Cog

Next add some scripts to package.json.

"scripts": {
  "build": "node build",
  "jsx": "jsx -x jsx src .",
  "watch:jsx": "jsx -w -x jsx src ."
}

Run the following scripts to compile Cog.jsx to plain JavaScript, and to create an SVG named cog-icon.svg

npm run jsx
npm run build

The xmlns Attribute in React

At the time of this writing, React strips the xmlns attribute from the SVG. When using the SVG inline in HTML, this shouldn’t be a problem, but when using it as an image, the attribute is needed. To get around this limitation, add a wrapping SVG tag in build.js.

var build = function(name, props) {

  props.size = props.size || 64
  var size = props.size
  var viewBox = [0, 0, size, size].join(' ')
  var svg = [
    '<svg xmlns="http://www.w3.org/2000/svg" ',
      'viewBox="', viewBox, '" ',
      'width="', size, '" ',
      'height="', size, '" ',
    '>',
    React.renderToStaticMarkup(React.createElement(Icon, props)),
    '</svg>'
  ].join('')
  
  fs.writeFileSync('icons/' + name + '.svg', svg)

}

After making these changes, run npm run build to rebuild the SVG. Open the SVG file in a browser to see the icon as it progresses. At this point, it should appear blank, but you can open web inspector to ensure that the SVG wrapper is there.

Watching Changes

To watch changes as the icon is developed, run the watch:jsx command to transpile the jsx file to js.

npm run watch:jsx

For each change you’ll need to rerun the build script. While you can also set up watching for the build script, this is beyond the scope of the tutorial.

Default Props

To allow for adjustments to be made from the build script, the icon will use React props. Define some defaults in Cog.jsx.

getDefaultProps: function() {
  return {
    size: 64,
    d1: 1,
    d2: .6875,
    d3: .375,
    teeth: 8,
    splay: 0.375,
    fill: 'currentcolor'
  }
},

The width and height of the square-shaped icon will be handled with the size prop. The diameters d1, d2, and d3 represent ratios of the size for each concentric circle. The number of teeth on the cog will be handled with the teeth prop. The splay prop represents the angle for the side of each tooth and will be explained later. And the fill prop is set to currentcolor to inherit color when used inline in HTML.

Defining Radii and Other Values

Within the render function, use these props to define other values that will be used to create the icon.

render: function() {

  var size = this.props.size
  var fill = this.props.fill

  // Center
  var c = size / 2

  // Radii
  var r1 = this.props.d1 * size / 2
  var r2 = this.props.d2 * size / 2
  var r3 = this.props.d3 * size / 2

  // Angle
  var angle = 360 / this.props.teeth
  var offset = 90

  var viewBox = [0, 0, size, size].join(' ')

  // ...

}

The center of the icon is defined as c. The angle for each tooth is calculated based on the number of teeth. And offset is used to rotate the icon 90° to ensure that the first tooth is at the top.

Path Data

The pathData variable will be used for the path element’s d attribute. The value for this attribute is a string of commands used to draw a line. Letters represent different commands and numbers represent positions in the x/y coordinate system. Uppercase letters are used for absolute coordinates, while lowercase is used for relative coordinates. This tutorial only uses absolute coordinates, so each letter must be uppercase. Only three commands will be used to create the icon: Move M, Line To L, and Arc A. To read more about the SVG path element, see this MDN tutorial.

The pathData variable is constructed with an array followed by the .join() method. This is to more easily combine path commands and numbers and ensure that the values have a space between each one.

To demonstrate how the path commands work, the following should create a rectangle based on the coordinates given.

var pathData = [
  'M', 2, 2, // Move to 2,2
  'L', 62, 2, // Draw a line to 62,2
  'L', 62, 62, // Draw a line to 62,62
  'L', 2, 62, // Draw a line to 2,62
  'L', 2, 2, // Draw a line to 2,2
].join(' ')

Building Teeth

To create teeth around the outer circle based on the number given in the teeth prop, a function will be used to return the values for each point. To calculate the x/y coordinates for each point around the outer circle, add the following functions.

var rad = function(a) {
  return Math.PI * a / 180
}

var rx = function(r, a) {
  return c + r * Math.cos(rad(a))
}

var ry = function(r, a) {
  return c + r * Math.sin(rad(a))
}

The rad function converts degrees to radians for convenience and since the Math.cos, Math.sin, and Math.tan functions all require radians. The rx and ry functions calculate the x and y coordinates respectively based on the radius and angle.

The rx and ry functions being used to find coordinates along a circle

Start with a polygon to see how the teeth prop can be adjusted to create different numbers of points.

var num = function(n) {
  return (n < 0.0000001) ? 0 : n 
}

var drawTeeth = function(n) {
  var d = []
  for (var i = 0; i < n; i++) {
    var a = angle * i - offset
    var line = [
      (i === 0) ? 'M' : 'L',
      num(rx(r1, a)),
      num(ry(r1, a)),
    ].join(' ')
    d.push(line)
  }
  return d.join(' ')
}

var pathData = [
  drawTeeth(this.props.teeth)
].join(' ')
Polygons with 8, 7, 6, and 5 points

Passing the number of teeth to the drawTeeth function, it loops through and calculates each angle with the offset defined above. Using a ternary operator, it either moves to the first point or draws a line to subsequent points. The x and y coordinates are calculated with the rx and ry functions. Then each command is pushed to the d array, and it is return as a string.

Since Math functions are calculating numbers that are being converted to strings for the d attribute, JavaScript will convert extremely small numbers to scientific notation. To prevent this from happening, the num function is used to return 0 for small numbers.

Shaping the Teeth

Now that the function to loop through the number of teeth is set up, change the commands in the line array to draw lines to the inner circle of the cog.

var drawTeeth = function(n) {
  var d = []
  for (var i = 0; i < n; i++) {
    var a = angle * i - offset
    var a1 = a + 6
    var a2 = a + angle - 6
    var line = [
      (i === 0) ? 'M' : 'L',
      num(rx(r1, a)),
      num(ry(r1, a)),
      'L',
      num(rx(r2, a1)),
      num(ry(r2, a1)),
      'A', r2, r2,
      '0 0 1',
      num(rx(r2, a2)),
      num(ry(r2, a2)),
    ].join(' ')
    d.push(line)
  }
  return d.join(' ')
}

The Arc Command

Here the Arc A command is being used to draw part of the inner circle. The first two values in the Arc command represent the x and y radii. The next three values are booleans representing the x-axis-rotation, large-arc-flag, and the sweep-flag. The sweep-flag value is set to 1 (true) to ensure the arc curves in the right direction. The last two values are the x and y coordinates for where the arc should end. To read more about the Arc command see this MDN tutorial.

Currently, this function arbitrarily adds and subtracts 6° to the angle to create the teeth. Add the following to the render function to calculate the angles based on the number of teeth.

var ta = angle / 4
var tw = Math.tan(rad(ta)) * r1

var tx = function(a, w) {
  return Math.sin(rad(a)) * w
}

var ty = function(a, w) {
  return Math.cos(rad(a)) * w
}

var drawTeeth = function(n) {
  var d = []
  for (var i = 0; i < n; i++) {
    var a = angle * i - offset
    var a1 = a + ta
    var a2 = a + angle - ta
    var line = [
      (i === 0) ? 'M' : 'L',
      num(rx(r1, a) + tx(a, tw)),
      num(ry(r1, a) - ty(a, tw)),
      'L',
      num(rx(r1, a) - tx(a, tw)),
      num(ry(r1, a) + ty(a, tw)),
      'L',
      num(rx(r2, a1)),
      num(ry(r2, a1)),
      'A', r2, r2,
      '0 0 1',
      num(rx(r2, a2)),
      num(ry(r2, a2)),
    ].join(' ')
    d.push(line)
  }
  return d.join(' ')
}

The tooth angle ta is one-fourth of the angle for each tooth. The tooth width tw is based on that angle. The tx and ty functions are used to calculate the x and y coordinates based on angle and distance. These functions and values are added to the line array to offset points for the corners of the teeth and the points at which they intersect the inner circle.

Splayed Teeth

The icon is starting to take shape, but the sides of each tooth are based on angles from the center. To splay the sides of the teeth in the other direction, edit the tw variable as shown below.

var ta = angle / 4
var splay = this.props.splay * ta
var tw = Math.tan(rad(ta - splay)) * r1

Splay, defined earlier in default props, represents a ratio of the tooth angle. Since it’s a prop, adjustments to this angle can be made from the build script. For the tooth width, the splay angle is subtracted from the tooth angle.

For the drawTeeth function, splay is added and subtracted from the angles at which the side of the tooth should intersect the inner circle.

var drawTeeth = function(n) {
  var d = []
  for (var i = 0; i < n; i++) {
    var a = angle * i - offset
    var a1 = a + ta + splay
    var a2 = a + angle - ta - splay
    var line = [
      (i === 0) ? 'M' : 'L',
      num(rx(r1, a) + tx(a, tw)),
      num(ry(r1, a) - ty(a, tw)),
      'L',
      num(rx(r1, a) - tx(a, tw)),
      num(ry(r1, a) + ty(a, tw)),
      'L',
      num(rx(r2, a1)),
      num(ry(r2, a1)),
      'A', r2, r2,
      '0 0 1',
      num(rx(r2, a2)),
      num(ry(r2, a2)),
    ].join(' ')
    d.push(line)
  }
  return d.join(' ')
}

Adding a Hole

Now all that’s left is to add the hole in the center. To create a circle that subtracts (or punches) from the outer shape, use two Arc commands to draw a circle in a counterclockwise direction. With the path element, intersecting shapes subtract from each other when they are drawn in opposite directions.

var hole = function() {
  return [
    'M', c, c - r3,
    'A', r3, r3,
    '0 0 0',
    c, c + r3,
    'A', r3, r3,
    '0 0 0',
    c, c - r3,
  ].join(' ')
}

var pathData = [
  drawTeeth(this.props.teeth),
  hole()
].join(' ')

Now you should have an adjustable cog icon similar to the one below. View the complete source code for the cog icon on GitHub. Hopefully you can see how with just a little bit of math, React can be a powerful tool for creating flexible and precise SVG graphics.

Live Demo

SVG Code

<svg viewBox="0 0 64 64" width="64" height="64" fill="currentcolor">
  <path d="M 28.053176443624363 0 L 35.94682355637564 0 L 37.86768066444777 10.796926552500324 A 22 22 0 0 1 42.84376022905525 12.858086195608351 L 51.8365912971095 6.5817573011704615 L 57.41824269882954 12.1634087028905 L 51.14191380439165 21.156239770944754 A 22 22 0 0 1 53.203073447499676 26.132319335552236 L 64 28.053176443624363 L 64 35.94682355637564 L 53.203073447499676 37.86768066444777 A 22 22 0 0 1 51.14191380439165 42.84376022905525 L 57.41824269882954 51.8365912971095 L 51.8365912971095 57.41824269882954 L 42.84376022905525 51.14191380439165 A 22 22 0 0 1 37.86768066444777 53.203073447499676 L 35.94682355637564 64 L 28.053176443624363 64 L 26.13231933555224 53.203073447499676 A 22 22 0 0 1 21.15623977094475 51.14191380439165 L 12.1634087028905 57.41824269882954 L 6.5817573011704615 51.8365912971095 L 12.858086195608351 42.84376022905525 A 22 22 0 0 1 10.796926552500327 37.867680664447775 L 0 35.94682355637565 L 0 28.05317644362437 L 10.79692655250032 26.132319335552246 A 22 22 0 0 1 12.858086195608351 21.156239770944744 L 6.581757301170455 12.163408702890502 L 12.163408702890493 6.581757301170461 L 21.156239770944747 12.858086195608351 A 22 22 0 0 1 26.132319335552232 10.796926552500324 M 32 20 A 12 12 0 0 0 32 44 A 12 12 0 0 0 32 20"></path>
</svg>

Questions, Comments, Suggestions?

Open an Issue