Even a front-end engineer who routinely cleans up messy SVG files may never expect to end up drawing with code. The turning point tends to arrive with a project that forces the numbers to matter — in this case, a Calligraphy Grid generator built last winter. Knowing the basic structures of SVG is one thing; understanding what all those numbers mean and how they interact is another. Once that clicks, hand-coding SVG becomes not just tolerable but interesting.
No
<path>aheadComplex shapes like paths are out of scope here. This is practical material for simple SVGs. For curves, a tool like Illustrator or Affinity remains the better recommendation — though a path is useful if you are into compounding lines (perhaps a Part 2 topic). The focus is on examples that illustrate the math behind drawing SVGs. For a deeper look at the specs, see “A Practical Guide To SVG And Design Tools.”
Drawing With Math: Coordinate Systems
Vector programs are essentially drawing on a coordinate system, and the resulting paths and shapes are stored as SVG files. Open one in an editor and you find a bunch of paths full of numbers — coordinates that make up the lines.
There is a difference, however, between the all-powerful <path> and more semantic elements like <rect>, <circle>, <line>, <ellipse>, <polygon>, and <polyline>. These are easy to read and write by hand, and they open up animation possibilities. SVGs are more than never-pixelated, infinitely scaling images; they can also be sizable pieces of code.
Units And The viewBox
Units in SVG can confuse at first. Because it is a vector format, units are detached from the browser and relative to the coordinate system you are working in. Rather than using a unit inside SVG, you use plain numbers and define the size of the document. Your width and height might use CSS rem units, but in the viewBox, units become a concept for establishing sizing relationships.
The viewBox works a bit like CSS aspect-ratio: it sets the relationship between the width and height of the coordinate system and defines the box you work in — think of it as the document size. Anything placed inside the SVG with dimensions larger than the viewBox will not be visible, so the viewBox acts as the cutout through which you view the coordinate system. The width and height attributes are unnecessary when a viewBox attribute is present.
With a viewBox, an SVG behaves much like a regular image. As with images, it is usually easiest to set either a width or a height and let the other dimension size itself from the intrinsic aspect ratio. A function that draws an SVG might therefore store three separate variables and fill them in like this:
`<svg
width="${svgWidth}"
viewBox="0 0 ${documentWidth} ${documentHeight}"
xmlns="http://www.w3.org/2000/svg"
>`;
Accessibility And Reuse
There is a lot to know about SVG: turning a frequently reused image into a symbol referenced by a use tag, creating sprites, icon best practices, and so on. That territory — optimizing and using SVG files, rather than designing them — is out of scope here. One thing worth implementing from the start is accessibility.
SVGs can live in an <img> tag, where alt tags are available, but you then lose the ability to interact with the SVG code. Inlining may be preferable. When inlining, it is easiest to declare role="img" and add a <title> tag with the image title.
Note: For SVG and accessibility recommendations, see this article.
<svg
role="img"
[...attr]
>
<title>An accessible title</title>
<!-- design code -->
</svg>
Generating SVG With JavaScript
Drawing SVGs usually involves some mathematics — mostly simple arithmetic, unless you are drawing calligraphy grids and digging out trigonometry. Even for simple math, most people would rather use algebra than write SVGs in pure HTML. Giving numbers meaning makes SVG code much easier to understand, so these examples stick to JavaScript; naming coordinates makes them far more legible. Each example will list the variables with the simple math, then show JSX-style templates for interpolation, which give more legible syntax highlighting than string interpolation. Each is available as a CodePen.
To stay framework-agnostic, the approach uses plain vanilla JavaScript: create a container element in HTML to hold the SVG, then grab that element with JavaScript.
<div data-svg-container></div>
<script src="template.js"></script>
For simplicity, the first drawing is a rectangle <rect> covering the entire viewBox with a fill.
Note: All valid CSS values work as fills — a fixed color, currentColor to pick up the site’s text color, or a CSS variable — which is useful when inlining an SVG that should interact with its page.
The variable setup comes first.
// vars
const container = document.querySelector("[data-svg-container]");
const svgWidth = "30rem"; // use any value with units here
const documentWidth = 100;
const documentHeight = 100;
const rectWidth = documentWidth;
const rectHeight = documentHeight;
const rectFill = "currentColor"; // use any color value here
const title = "A simple square box";
Method 1: Create Element And Set Attributes
Keeping type safety is easier with this method — it uses proper SVG elements and attributes — but it is less performant and can take a long time with many elements.
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
const titleElement = document.createElementNS("http://www.w3.org/2000/svg", "title");
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
svg.setAttribute("width", svgWidth);
svg.setAttribute("viewBox", `0 0 ${documentWidth} ${documentHeight}`);
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("role", "img");
titleElement.textContent = title;
rect.setAttribute("width", rectWidth);
rect.setAttribute("height", rectHeight);
rect.setAttribute("fill", rectFill);
svg.appendChild(titleElement);
svg.appendChild(rect);
container.appendChild(svg);
See the Pen [SVG Rectangle (JS Method 1) [forked]](https://codepen.io/smashingmag/pen/LYKKVzg) by Myriam.
Method 2: Create An SVG String
Alternatively, build an SVG string and set the container’s innerHTML. This is more performant, but type safety is lost and the elements are not properly created in the DOM.
container.innerHTML = `
<svg
width="${svgWidth}"
viewBox="0 0 ${documentWidth} ${documentHeight}"
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<title>${title}</title>
<rect
width="${rectWidth}"
height="${rectHeight}"
fill="${rectFill}"
/>
</svg>`;
See the Pen [SVG Rectangle (JS Method 2) [forked]](https://codepen.io/smashingmag/pen/BaggNmN) by Myriam.
Method 3: Best Of Both Worlds
The middle path: create the SVG itself as a DOM element, then set its content via innerHTML. A proper SVG element gets appended to the container, so it can be type-checked and accessed correctly. Since SVG content rarely changes after creation, this is likely the best approach.
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", svgWidth);
svg.setAttribute("viewBox", `0 0 ${documentWidth} ${documentHeight}`);
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("role", "img");
svg.innerHTML = `
<title>${title}</title>
<rect
width="${rectWidth}"
height="${rectHeight}"
fill="${rectFill}"
/>
`;
container.appendChild(svg);
See the Pen [SVG Rectangle (JS Method 3) [forked]](https://codepen.io/smashingmag/pen/RwzzPjz) by Myriam.
Basic Elements In Practice
Rectangles
<rect> creates a box. Its x and y attributes define the position of the top-left corner and are optional; without them the box is drawn at the origin (0,0), as in the previous example. The rx and ry attributes are radii. Defining rx automatically sets ry to the same value unless ry is redeclared — in which case you get an elliptical corner radius instead of a circular one.
The next SVG draws four rectangles, one per quadrant:
- Top left: a plain rectangle with top and left offset, width, and height.
- Top right: a small corner radius makes it a rounded rectangle.
- Bottom left: a corner radius large enough that it becomes a circle. The box origin is a bit odd, but it works.
- Bottom right: an elliptical corner radius produces this squoval shape.
Here is the JavaScript implementation:
const rectDocWidth = 200;
const rectDocHeight = 200;
const rectFill = "currentColor";
const docOffset = 15;
const rectSize = rectDocWidth / 2 - docOffset * 2;
const roundedCornerRadius = 10;
const circleLookRadius = rectSize / 2;
const ellipticalRy = roundedCornerRadius * 2;
These variables then get applied to the template to set up the SVG:
<svg
width={svgWidth}
viewBox={`0 0 ${rectDocWidth} ${rectDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<title>Four Rectangles of different qualities placed in each quadrant</title>
<rect
x={docOffset}
y={docOffset}
width={rectSize}
height={rectSize}
fill={rectFill}
/>
<rect
x={rectDocWidth - rectSize - docOffset}
rx={roundedCornerRadius}
y={docOffset}
width={rectSize}
height={rectSize}
fill={rectFill}
/>
<rect
x={docOffset}
rx={circleLookRadius}
y={rectDocHeight - rectSize - docOffset}
width={rectSize}
height={rectSize}
fill={rectFill}
/>
<rect
x={rectDocWidth - rectSize - docOffset}
rx={roundedCornerRadius}
ry={ellipticalRy}
y={rectDocHeight - rectSize - docOffset}
width={rectSize}
height={rectSize}
fill={rectFill}
/>
</svg>
And the result:
See the Pen [SVG Rect [forked]](https://codepen.io/smashingmag/pen/mdZZJpN) by Myriam.
Lines, Circles And Ellipses
The <line> element takes x1, y1, x2, and y2 — the coordinates of its start and end points. Axis-aligned lines follow a simple rule: the y values match for a horizontal line, and the x values match for a vertical line.
Drawing a horizontal and a vertical line through the center of the document works with deliberately odd numbers, because floating point values are fine in SVG and, unlike some CSS cases, you don't run into fractional-pixel or subpixel rendering problems.
const lineDocWidth = 421;
const lineDocHeight = 391;
const lineStroke = "currentColor";
const lineStrokeWidth = 5;
const horizontalLineStart = 0;
const horizontalLineEnd = lineDocWidth;
const horizontalLineY = lineDocHeight / 2;
const verticalLineStart = 0;
const verticalLineEnd = lineDocHeight;
const verticalLineX = lineDocWidth / 2;
<svg
width={svgWidth}
viewBox={`0 0 ${lineDocWidth} ${lineDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<title>Horizontal and Vertical Line through the middle of the document</title>
<line
x1={horizontalLineStart}
x2={horizontalLineEnd}
y1={horizontalLineY}
y2={horizontalLineY}
stroke={lineStroke}
stroke-width={lineStrokeWidth}
/>
<line
x1={verticalLineX}
x2={verticalLineX}
y1={verticalLineStart}
y2={verticalLineEnd}
stroke={lineStroke}
stroke-width={lineStrokeWidth}
/>
</svg>
See the Pen [SVG Line [forked]](https://codepen.io/smashingmag/pen/abggOqK) by Myriam.
Circles
<circle> uses cx, cy, and r: the center coordinates and the radius. The mental shift is that r is a radius, not a diameter, and the coordinates address the center rather than the edge.
To place a circle whose outer edge sits a given distance from the bottom left corner and whose diameter is known, the coordinates have to be computed from that offset and size.
const circleDocWidth = 100;
const circleDocHeight = 100;
const circleOffset = 10;
const circleDiameter = 20;
const circleRadius = circleDiameter / 2;
const circleX = circleOffset + circleRadius;
const circleY = circleDocHeight - circleOffset - circleRadius;
<svg
width={svgWidth}
viewBox={`0 0 ${circleDocWidth} ${circleDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<circle
cx={circleX}
cy={circleY}
r={circleRadius}
fill="red"
/>
</svg>
See the Pen [SVG Circle [forked]](https://codepen.io/smashingmag/pen/KKjjpoW) by Myriam.
Ellipses
<ellipse> takes cx, cy, rx, and ry — center coordinates plus both radii. Offsetting an ellipse from the top right corner with a horizontal radius of one size and half that for the vertical radius needs the same kind of variable setup.
const ellipseSVGWidth = 100;
const ellipseDocWidth = 100;
const ellipseDocHeight = 100;
const ellipseOffset = 10;
const ellipseHorizontalRadius = ellipseDocWidth / 2 - ellipseOffset;
const ellipseVerticalRadius = ellipseHorizontalRadius / 2;
const ellipseX = ellipseDocWidth - ellipseOffset - ellipseHorizontalRadius;
const ellipseY = ellipseOffset + ellipseVerticalRadius;
<svg
width={svgWidth}
viewBox={`0 0 ${ellipseDocWidth} ${ellipseDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<title>Ellipse offset from the top right corner</title>
<ellipse
cx={ellipseX}
cy={ellipseY}
rx={ellipseHorizontalRadius}
ry={ellipseVerticalRadius}
fill="hotpink"
/>
</svg>
See the Pen [SVG Ellipse [forked]](https://codepen.io/smashingmag/pen/NWZZqMR) by Myriam.
Multiple Points: <polyline> And <polygon>
For a shape made of many points that is neither a rectangle nor a circle, polyline and polygon share the same attributes and differ only in that a polygon connects the last point back to the first while a polyline does not. Both take a points attribute: x and y values separated by spaces. Both carry a fill by default, which is often surprising on a polyline, so setting it to none is usually what you want.
Three circles with lines connecting their centers can be built by chaining each circle's cx and cy into the points attribute. Because SVG paints from background to foreground, the circles are drawn first and the lines stack on top.
As with the repeated circles, the composite can be drawn four times to make the polyline/polygon difference visible. With more than one element per set, the g element groups them so attributes apply to all children at once. Instead of adjusting x and y for every element, a transform on the group pushes the whole composite into each quadrant: transform="translate(x,y)". The translate behaves much like the CSS equivalent, though the syntax differs slightly; it moves the group's original position along the x and y axes.
With identical coordinates, the polyline omits the line between the blue and the red dot while the polygon draws it. Under a fill, though, both shapes interpret the data as if the shape were closed — visible on the right of the graphic, where the polyline appears to be missing a piece of a circle.
The repeated markup can be condensed with JavaScript: objects for the circles, chained cx and cy values for the points attribute, and transforms stored in variables.
const polyDocWidth = 200;
const polyDocHeight = 200;
const circleOne = { cx: 25, cy: 80, r: 10, fill: "red" };
const circleTwo = { cx: 40, cy: 20, r: 5, fill: "lime" };
const circleThree = { cx: 70, cy: 60, r: 8, fill: "cyan" };
const points = `${circleOne.cx},${circleOne.cy} ${circleTwo.cx},${circleTwo.cy} ${circleThree.cx},${circleThree.cy}`;
const moveToTopRight = `translate(${polyDocWidth / 2}, 0)`;
const moveToBottomRight = `translate(${polyDocWidth / 2}, ${polyDocHeight / 2})`;
const moveToBottomLeft = `translate(0, ${polyDocHeight / 2})`;
<svg
width={svgWidth}
viewBox={`0 0 ${polyDocWidth} ${polyDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<title>Composite shape comparison</title>
<g>
<circle
cx={circleOne.cx}
cy={circleOne.cy}
r={circleOne.r}
fill={circleOne.fill}
/>
<circle
cx={circleTwo.cx}
cy={circleTwo.cy}
r={circleTwo.r}
fill={circleTwo.fill}
/>
<circle
cx={circleThree.cx}
cy={circleThree.cy}
r={circleThree.r}
fill={circleThree.fill}
/>
<polyline
points={points}
fill="none"
stroke="black"
/>
</g>
<g transform={moveToTopRight}>
<circle
cx={circleOne.cx}
cy={circleOne.cy}
r={circleOne.r}
fill={circleOne.fill}
/>
<circle
cx={circleTwo.cx}
cy={circleTwo.cy}
r={circleTwo.r}
fill={circleTwo.fill}
/>
<circle
cx={circleThree.cx}
cy={circleThree.cy}
r={circleThree.r}
fill={circleThree.fill}
/>
<polyline
points={points}
fill="white"
stroke="black"
/>
</g>
<g transform={moveToBottomLeft}>
<circle
cx={circleOne.cx}
cy={circleOne.cy}
r={circleOne.r}
fill={circleOne.fill}
/>
<circle
cx={circleTwo.cx}
cy={circleTwo.cy}
r={circleTwo.r}
fill={circleTwo.fill}
/>
<circle
cx={circleThree.cx}
cy={circleThree.cy}
r={circleThree.r}
fill={circleThree.fill}
/>
<polygon
points={points}
fill="none"
stroke="black"
/>
</g>
<g transform={moveToBottomRight}>
<circle
cx={circleOne.cx}
cy={circleOne.cy}
r={circleOne.r}
fill={circleOne.fill}
/>
<circle
cx={circleTwo.cx}
cy={circleTwo.cy}
r={circleTwo.r}
fill={circleTwo.fill}
/>
<circle
cx={circleThree.cx}
cy={circleThree.cy}
r={circleThree.r}
fill={circleThree.fill}
/>
<polygon
points={points}
fill="white"
stroke="black"
/>
</g>
</svg>
See the Pen [SVG Polygon / Polyline (simple) [forked]](https://codepen.io/smashingmag/pen/OJeeVoM) by Myriam.
Collapsing The Repetition
The composite is a candidate for optimization: three circle elements that follow one pattern, one repeated block that swaps the fill style, another repetition that substitutes a polyline for a polygon, and four transform variants (no transform counts as one). That shape of the problem calls for nested loops.
A vanilla implementation is the clearest place to demonstrate this, since loop syntax varies widely between frameworks. The approach here is arrays for each kind of variation plus a helper function that walks the data and produces an array of objects holding everything each group needs. On such a short array, one could reduce to a single element with repeated values, but this version takes DRY seriously. Generator functions per element type would be a more generic route; this is just a sketch of what the logic can do. Looping over the group array then builds the SVG markup.
const container = document.querySelector("[data-svg-container]");
const svgWidth = 200;
const documentWidth = 200;
const documentHeight = 200;
const halfWidth = documentWidth / 2;
const halfHeight = documentHeight / 2;
const circles = [
{ cx: 25, cy: 80, r: 10, fill: "red" },
{ cx: 40, cy: 20, r: 5, fill: "lime" },
{ cx: 70, cy: 60, r: 8, fill: "cyan" },
];
const points = circles.map(({ cx, cy }) => `${cx},${cy}`).join(" ");
const elements = ["polyline", "polygon"];
const fillOptions = ["none", "white"];
const transforms = [
undefined,
`translate(${halfWidth}, 0)`,
`translate(0, ${halfHeight})`,
`translate(${halfWidth}, ${halfHeight})`,
];
const makeGroupsDataObject = () => {
let counter = 0;
const g = [];
elements.forEach((element) => {
fillOptions.forEach((fill) => {
const transform = transforms[counter++];
g.push({ element, fill, transform });
});
});
return g;
};
const groups = makeGroupsDataObject();
// result:
// [
// {
// element: "polyline",
// fill: "none",
// },
// {
// element: "polyline",
// fill: "white",
// transform: "translate(100, 0)",
// },
// {
// element: "polygon",
// fill: "none",
// transform: "translate(0, 100)",
// },
// {
// element: "polygon",
// fill: "white",
// transform: "translate(100, 100)",
// }
// ]
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("width", svgWidth);
svg.setAttribute("viewBox", `0 0 ${documentWidth} ${documentHeight}`);
svg.setAttribute("xmlns", "http://www.w3.org/2000/svg");
svg.setAttribute("role", "img");
svg.innerHTML = "<title>Composite shape comparison</title>";
groups.forEach((groupData) => {
const circlesHTML = circles
.map((circle) => {
return `
<circle
cx="${circle.cx}"
cy="${circle.cy}"
r="${circle.r}"
fill="${circle.fill}"
/>`;
})
.join("");
const polyElementHTML = `
<${groupData.element}
points="${points}"
fill="${groupData.fill}"
stroke="black"
/>`;
const group = `
<g ${groupData.transform ? `transform="${groupData.transform}"` : ""}>
${circlesHTML}
${polyElementHTML}
</g>
`;
svg.innerHTML += group;
});
container.appendChild(svg);
See the Pen [SVG Polygon / Polyline (JS loop version) [forked]](https://codepen.io/smashingmag/pen/XWLLbPq) by Myriam.
Patterns In <defs>
The basics barely scratch the surface — there is more to transform, plus mask, marker, and other features. Two of them are worth showing because they were the starting point for the Calligraphy Grids work, though a generator can't use them if the output has to open in Affinity, which doesn't support pattern. pattern lives inside the defs section, where reusable elements are defined and then referenced elsewhere in the SVG.
Graph Grid
A graph is just repeated horizontal and vertical lines across the x- and y-axes, which is exactly what pattern is for. A <rect> references a pattern through its fill attribute, and the pattern's own width, height, and viewBox determine how it repeats. The goal in this case: a grid perfectly centered at any width or height, with a definable cell size.
const graphDocWidth = 226;
const graphDocHeight = 101;
const cellSize = 5;
const strokeWidth = 0.3;
const strokeColor = "currentColor";
const patternHeight = (cellSize / graphDocHeight) * 100;
const patternWidth = (cellSize / graphDocWidth) * 100;
const gridYStart = (graphDocHeight % cellSize) / 2;
const gridXStart = (graphDocWidth % cellSize) / 2;
<svg
width={svgWidth}
viewBox={`0 0 ${graphDocWidth} ${graphDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<defs>
<pattern
id="horizontal"
viewBox={`0 0 ${graphDocWidth} ${strokeWidth}`}
width="100%"
height={`${patternHeight}%`}
>
<line
x1="0"
x2={graphDocWidth}
y1={gridYStart}
y2={gridYStart}
stroke={strokeColor}
stroke-width={strokeWidth}
/>
</pattern>
<pattern
id="vertical"
viewBox={`0 0 ${strokeWidth} ${graphDocHeight}`}
width={`${patternWidth}%`}
height="100%"
>
<line
y1={0}
y2={graphDocHeight}
x1={gridXStart}
x2={gridXStart}
stroke={strokeColor}
stroke-width={strokeWidth}
/>
</pattern>
</defs>
<title>A graph grid</title>
<rect
width={graphDocWidth}
height={graphDocHeight}
fill="url(#horizontal)"
/>
<rect
width={graphDocWidth}
height={graphDocHeight}
fill="url(#vertical)"
/>
</svg>
See the Pen [SVG Graph Grid [forked]](https://codepen.io/smashingmag/pen/XWLLbxq) by Myriam.
Dot Grid
A dot grid can repeat a circle, or it can use a single line with stroke-dasharray and stroke-dashoffset to produce dashes.
const dotDocWidth = 219;
const dotDocHeight = 100;
const cellSize = 4;
const strokeColor = "black";
const gridYStart = (dotDocHeight % cellSize) / 2;
const gridXStart = (dotDocWidth % cellSize) / 2;
const dotSize = 0.5;
const patternHeight = (cellSize / dotDocHeight) * 100;
<svg
width={svgWidth}
viewBox={`0 0 ${dotDocWidth} ${dotDocHeight}`}
xmlns="http://www.w3.org/2000/svg"
role="img"
>
<defs>
<pattern
id="horizontal-dotted-line"
viewBox={`0 0 ${dotDocWidth} ${dotSize}`}
width="100%"
height={`${patternHeight}%`}
>
<line
x1={gridXStart}
y1={gridYStart}
x2={dotDocWidth}
y2={gridYStart}
stroke={strokeColor}
stroke-width={dotSize}
stroke-dasharray={`0,${cellSize}`}
stroke-linecap="round"
></line>
</pattern>
</defs>
<title>A Dot Grid</title>
<rect
x="0"
y="0"
width={dotDocWidth}
height={dotDocHeight}
fill="url(#horizontal-dotted-line)"
></rect>
</svg>
See the Pen [SVG Dot Grid [forked]](https://codepen.io/smashingmag/pen/eYwwNQM) by Myriam.



