The SVG tag in HTML, from viewBox to paths

An <svg> element is a small drawing written as markup inside your page. Learn its coordinate system, six shapes and a handful of attributes, and you can draw icons, badges and charts by hand.

The <svg> tag puts a vector drawing straight into an HTML page. Inside it you place shapes such as <rect>, <circle> and <path>, positioned on a grid you declare with the viewBox attribute.

The browser scales that grid to the element's size, so the drawing stays sharp at any size.

Here is the smallest useful example:

<svg viewBox="0 0 200 100" width="300">
  <rect x="10" y="10" width="80" height="80" fill="#2563eb"/>
  <circle cx="150" cy="50" r="40" fill="#f59e0b"/>
</svg>

The quickest way to understand viewBox is to move it. Drag the sliders and watch the drawing pan, zoom and stretch.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>viewBox explorer</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  .stage { background: #fff; border: 1px dashed #b8c0cc; display: inline-block; line-height: 0; }
  .controls { display: grid; grid-template-columns: 112px 1fr 40px; gap: 6px 10px; align-items: center; margin-top: 12px; max-width: 460px; font-size: 14px; }
  .controls input { width: 100%; margin: 0; }
  output { font: 600 13px ui-monospace, Consolas, monospace; text-align: right; }
  pre { background: #1d2330; color: #e6edf3; padding: 10px 12px; border-radius: 8px; font-size: 13px; white-space: pre-wrap; word-break: break-all; margin: 12px 0 0; max-width: 460px; }
</style>
</head>
<body>
<div class="stage">
  <!-- the drawing is made in a 200 x 100 coordinate space -->
  <svg id="pic" width="300" height="150" viewBox="0 0 200 100">
    <rect x="0" y="0" width="200" height="100" fill="#eef4ff"/>
    <circle cx="60" cy="50" r="35" fill="#2563eb"/>
    <rect x="110" y="20" width="70" height="60" rx="8" fill="#f59e0b"/>
    <text x="100" y="95" font-size="10" text-anchor="middle" fill="#1d2330">200 x 100 units</text>
  </svg>
</div>

<div class="controls">
  <label for="vx">viewBox x</label><input id="vx" type="range" min="-100" max="150" value="0"><output></output>
  <label for="vy">viewBox y</label><input id="vy" type="range" min="-50" max="80" value="0"><output></output>
  <label for="vw">viewBox width</label><input id="vw" type="range" min="40" max="400" value="200"><output></output>
  <label for="vh">viewBox height</label><input id="vh" type="range" min="20" max="200" value="100"><output></output>
  <label for="ew">element width</label><input id="ew" type="range" min="100" max="340" value="300"><output></output>
  <label for="eh">element height</label><input id="eh" type="range" min="50" max="220" value="150"><output></output>
</div>
<pre id="code"></pre>

<script>
  const pic = document.getElementById('pic');
  const val = (id) => document.getElementById(id).value;

  function update() {
    // viewBox picks which part of the drawing to show; width/height set the box on the page
    const vb = `${val('vx')} ${val('vy')} ${val('vw')} ${val('vh')}`;
    pic.setAttribute('viewBox', vb);
    pic.setAttribute('width', val('ew'));
    pic.setAttribute('height', val('eh'));
    document.querySelectorAll('.controls input').forEach((i) => { i.nextElementSibling.textContent = i.value; });
    document.getElementById('code').textContent =
      `<svg width="${val('ew')}" height="${val('eh')}" viewBox="${vb}">`;
  }

  document.querySelectorAll('.controls input').forEach((i) => i.addEventListener('input', update));
  update();
</script>
</body>
</html>
The drawing is made in 200 x 100 units. The viewBox chooses which part you see, and width and height set the box on the page.

This guide covers the element itself: coordinates, shapes, paint, groups, text, CSS and accessibility. For using SVG as a page diagram, see SVG in HTML.

viewBox and the coordinate system

viewBox="min-x min-y width height" declares the drawing's own units. Every coordinate inside the <svg> is measured in those units, not in pixels.

A 200 x 100 viewBox drawn into a 300 x 150 box. Every coordinate is multiplied by 1.5.
A 200 x 100 viewBox drawn into a 300 x 150 box. Every coordinate is multiplied by 1.5.
  • The origin 0,0 is the top-left corner, and y grows downward.
  • Raising min-x or min-y pans the view. The shapes do not move; the window onto them does.
  • A smaller viewBox width zooms in, because fewer units fill the same box.

When the viewBox and the element have different proportions, the browser keeps the drawing's shape and centres it, leaving empty space. Try a tall element in the demo above to see it.

Changing that behaviour is the job of preserveAspectRatio, covered in SVG scale to fit.

width and height against viewBox

The two do different jobs. width and height set how big the box is on the page. viewBox sets what is drawn inside it.

You set What you get
Nothing A 300 x 150 box, and shapes are drawn 1 unit = 1 pixel
width and height only A fixed box; the drawing does not scale with it
viewBox only Fills the container width, height from the viewBox proportions
viewBox and a CSS width Scales to that width; the usual choice for responsive graphics
All three A fixed-size box with the drawing scaled into it

A good default is a viewBox on the element and the size in CSS, such as width: 100% or width: 1.2em for an icon.

The basic shapes

Six shapes cover most drawings, plus <path> for everything else. Add them in the playground and edit the numbers.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SVG shape playground</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  svg { display: block; width: 100%; max-width: 300px; background: #fff; border: 1px solid #d5d9e0; border-radius: 8px; }
  .add { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; }
  .add button { font: 600 13px ui-monospace, Consolas, monospace; padding: 6px 10px; border: 1px solid #c7ced8; border-radius: 6px; background: #fff; cursor: pointer; }
  .item { background: #fff; border: 1px solid #e1e4ea; border-radius: 8px; padding: 5px 8px; margin-bottom: 5px; font-size: 13px; }
  .item b { font-family: ui-monospace, Consolas, monospace; margin-right: 6px; }
  .item label { display: inline-flex; gap: 3px; align-items: center; margin: 2px 8px 2px 0; font-family: ui-monospace, Consolas, monospace; }
  .item input { width: 58px; font: 12px ui-monospace, Consolas, monospace; }
  .item input.wide { width: 170px; }
  pre { background: #1d2330; color: #e6edf3; padding: 10px 12px; border-radius: 8px; font-size: 12px; white-space: pre-wrap; word-break: break-all; max-height: 150px; overflow: auto; margin: 8px 0 0; }
</style>
</head>
<body>
<svg id="canvas" viewBox="0 0 200 120"></svg>
<div class="add">
  <button data-shape="rect">+ rect</button>
  <button data-shape="circle">+ circle</button>
  <button data-shape="line">+ line</button>
  <button data-shape="polygon">+ polygon</button>
  <button data-shape="path">+ path</button>
</div>
<div id="list"></div>
<pre id="code"></pre>

<script>
  const NS = 'http://www.w3.org/2000/svg';  // SVG elements must be created in this namespace
  const canvas = document.getElementById('canvas');

  // starting attributes for each shape
  const DEFAULTS = {
    rect:    { x: 20, y: 20, width: 60, height: 40, fill: '#2563eb' },
    circle:  { cx: 140, cy: 45, r: 25, fill: '#f59e0b' },
    line:    { x1: 20, y1: 100, x2: 180, y2: 80, stroke: '#1d2330', 'stroke-width': 3 },
    polygon: { points: '100,70 130,110 70,110', fill: '#16a34a' },
    path:    { d: 'M 20 70 Q 60 20 100 70 T 180 70', fill: 'none', stroke: '#db2777', 'stroke-width': 3 },
  };

  function addShape(type) {
    const el = document.createElementNS(NS, type);
    for (const [k, v] of Object.entries(DEFAULTS[type])) el.setAttribute(k, v);
    canvas.appendChild(el);

    // one row per shape, one input per attribute
    const row = document.createElement('div');
    row.className = 'item';
    row.innerHTML = `<b>&lt;${type}&gt;</b>`;
    for (const k of Object.keys(DEFAULTS[type])) {
      const label = document.createElement('label');
      const input = document.createElement('input');
      input.value = el.getAttribute(k);
      if (k === 'points' || k === 'd') input.className = 'wide';
      input.addEventListener('input', () => { el.setAttribute(k, input.value); showCode(); });
      label.append(k, input);
      row.appendChild(label);
    }
    document.getElementById('list').appendChild(row);
    showCode();
  }

  function showCode() {
    // the svg's own markup, one element per line
    document.getElementById('code').textContent = canvas.outerHTML
      .replace(/><\/(rect|circle|line|polygon|path)>/g, '/>')  // empty shapes as <rect ... />
      .replace(/><(?!\/svg)/g, '>\n  <').replace('></svg>', '>\n</svg>');
  }

  document.querySelectorAll('.add button').forEach((b) =>
    b.addEventListener('click', () => addShape(b.dataset.shape)));
  addShape('rect');
</script>
</body>
</html>
Each button adds a shape with starting attributes. Change any value and the markup below updates.
Element Key attributes Draws
<rect> x y width height rx A rectangle; rx rounds the corners
<circle> cx cy r A circle from its centre
<ellipse> cx cy rx ry An oval with two radii
<line> x1 y1 x2 y2 One straight segment
<polyline> points Joined segments, left open
<polygon> points Joined segments, closed
<path> d Any outline, from commands

A <line> is invisible until it has a stroke, because the default stroke is none and a line has no inside to fill.

Paths: the d attribute

A <path> is a pen following commands. Each command is a letter followed by numbers.

<path d="M 10 80 L 50 20 L 90 80 Z" fill="#16a34a"/>
<path d="M 10 80 l 40 -60 l 40 60 z" fill="#16a34a"/>

Both lines draw the same triangle. M moves the pen, L draws a line, and Z closes back to the start. Upper-case letters use absolute coordinates. Lower-case letters are relative to where the pen stopped, so l 40 -60 means "40 right, 60 up".

Other commands you will meet: H and V for horizontal and vertical lines, Q and C for curves, and A for arcs. When a path shoots off in the wrong direction, check the case of its letters first.

Fill, stroke and stroke-width

fill paints the inside of a shape and defaults to black. stroke paints the outline and defaults to none. stroke-width sets the outline's thickness in viewBox units.

The stroke is centred on the shape's edge. Half of it falls inside the shape and half outside, which is why a shape touching the viewBox edge loses part of its outline.

A stroke is centred on the edge. Inset the shape by half the stroke width, or widen the viewBox.
A stroke is centred on the edge. Inset the shape by half the stroke width, or widen the viewBox.

Other stroke attributes worth knowing: stroke-linecap="round" for rounded line ends, stroke-linejoin="round" for rounded corners, and fill="none" for an outline-only shape.

Groups, transform and text

<g> groups shapes. Attributes on a group, such as fill, are inherited by the shapes inside. A transform on the group moves them together:

<g transform="translate(60 0)" fill="#2563eb">
  <rect width="40" height="90"/>
  <text x="20" y="108" text-anchor="middle">Tue</text>
</g>

translate(x y) shifts, rotate(deg cx cy) turns around a point, and scale(n) resizes, including the stroke width.

<text> places a label with its baseline at y, not its top. text-anchor="middle" centres it on x. SVG text does not wrap lines on its own, and its font size is in viewBox units, so it grows and shrinks with the drawing.

Styling SVG with CSS

Inline SVG shapes are elements in your page, so ordinary CSS reaches them. Classes, :hover, media queries and CSS variables all work on fill and stroke.

.bar   { fill: var(--accent); }
.icon  { color: #2563eb; }
.icon circle { fill: currentColor; }

currentColor takes the element's CSS color, so an icon matches the text around it. The finished example below builds a badge and a chart from rectangles, a path and text, and recolours all of it by switching one class.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SVG badge and chart</title>
<style>
  body { margin: 0; padding: 16px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  /* the whole palette lives in three variables */
  .card { --accent: #2563eb; --track: #e5e9f0; --ink: #1d2330;
          background: #fff; border-radius: 12px; padding: 14px; max-width: 380px; }
  .card.green  { --accent: #16a34a; --track: #e3f3e8; }
  .card.orange { --accent: #ea580c; --track: #fdeadf; }
  .card.dark   { --accent: #60a5fa; --track: #334155; --ink: #e2e8f0; background: #1e293b; }

  svg { display: block; width: 100%; height: auto; }
  .bar   { fill: var(--accent); }
  .track { fill: var(--track); }
  .label { fill: var(--ink); font: 600 12px system-ui, sans-serif; }
  .value { fill: var(--ink); font: 700 26px system-ui, sans-serif; }
  .icon  { color: var(--accent); }  /* the icon paints with currentColor */

  .themes { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 12px; }
  .themes button { padding: 6px 12px; border: 1px solid #c7ced8; border-radius: 6px; background: #fff; cursor: pointer; font-size: 14px; }
</style>
</head>
<body>
<div class="card" id="card">
  <svg viewBox="0 0 320 190" role="img" aria-labelledby="t1">
    <title id="t1">Weekly sign-ups: Mon 12, Tue 18, Wed 9, Thu 22, Fri 15. Total 76.</title>

    <!-- icon: a circle and a check mark, coloured by currentColor -->
    <g class="icon" transform="translate(10 10)">
      <circle cx="16" cy="16" r="16" fill="currentColor"/>
      <path d="M9 16 l5 5 l9 -10" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"/>
    </g>
    <text class="label" x="52" y="22">Weekly sign-ups</text>
    <text class="value" x="52" y="48">76</text>

    <!-- bar chart: each day is a group of track + bar + label, moved sideways with transform -->
    <g transform="translate(20 66)">
      <g transform="translate(0 0)"><rect class="track" width="40" height="90" rx="4"/><rect class="bar" y="41" width="40" height="49" rx="4"/><text class="label" x="20" y="108" text-anchor="middle">Mon</text></g>
      <g transform="translate(60 0)"><rect class="track" width="40" height="90" rx="4"/><rect class="bar" y="16" width="40" height="74" rx="4"/><text class="label" x="20" y="108" text-anchor="middle">Tue</text></g>
      <g transform="translate(120 0)"><rect class="track" width="40" height="90" rx="4"/><rect class="bar" y="53" width="40" height="37" rx="4"/><text class="label" x="20" y="108" text-anchor="middle">Wed</text></g>
      <g transform="translate(180 0)"><rect class="track" width="40" height="90" rx="4"/><rect class="bar" y="0" width="40" height="90" rx="4"/><text class="label" x="20" y="108" text-anchor="middle">Thu</text></g>
      <g transform="translate(240 0)"><rect class="track" width="40" height="90" rx="4"/><rect class="bar" y="29" width="40" height="61" rx="4"/><text class="label" x="20" y="108" text-anchor="middle">Fri</text></g>
    </g>
  </svg>
</div>

<div class="themes">
  <button data-theme="">Blue</button>
  <button data-theme="green">Green</button>
  <button data-theme="orange">Orange</button>
  <button data-theme="dark">Dark</button>
</div>

<script>
  const card = document.getElementById('card');
  // one class swaps the CSS variables; every shape follows
  document.querySelectorAll('.themes button').forEach((b) =>
    b.addEventListener('click', () => { card.className = ('card ' + b.dataset.theme).trim(); }));
</script>
</body>
</html>
Every shape is painted from three CSS variables. The icon uses currentColor. Screen readers read the title.

A fill attribute on a shape counts less than any CSS rule that targets that shape, so .bar { fill: red } wins over fill="blue".

A rule on the <svg> itself, though, is only inherited, and a shape's own attribute beats inherited values. Changing SVG colour walks through the cases.

Accessibility

Decide first whether the drawing means something.

  • It carries information (a chart, a status badge): add role="img" to the <svg> and a <title> as its first child. Pointing aria-labelledby at the title's id makes the name explicit. Put the actual values in the title, as the badge demo does.
  • It is decoration (an icon next to a visible word): add aria-hidden="true" so screen readers skip it.
<svg viewBox="0 0 320 190" role="img" aria-labelledby="t1">
  <title id="t1">Weekly sign-ups: 76, highest on Thursday.</title>
  ...
</svg>

For icon-only buttons, label the button with aria-label and hide the SVG.

Inline svg against img src

The same file can go into a page two ways, and they behave differently.

An SVG in an img tag is a sealed picture. Inline, each shape is part of the page.
An SVG in an img tag is a sealed picture. Inline, each shape is part of the page.

<img src="chart.svg" alt="..."> treats the file as a picture. It can be cached and reused across pages, but the page's CSS cannot restyle its shapes and scripts inside the file do not run.

Inline <svg> gives up that separate caching in exchange for full CSS and JavaScript reach.

Use <img> for large, fixed illustrations. Use inline for icons, charts and anything that changes colour or reacts to the user. If you are serving SVG files to others, read hosting an SVG file first.

When it does not work

What you see Cause Fix
The SVG does not appear at all It has only a viewBox and sits in a shrink-to-fit parent, such as an inline-block or absolutely positioned box, so its width collapses to zero Give the <svg> a CSS width
A shape is missing Its coordinates are outside the viewBox, or it is a line with no stroke Check the numbers against the viewBox; add stroke
The outline is thinner on the edges Half the stroke falls outside the viewBox Inset the shape by half the stroke width, or widen the viewBox
CSS fill has no effect The SVG is in an <img> tag, or a style="fill:..." on the shape beats the rule Inline the SVG; remove the inline style
A rule on svg does not recolour shapes Each shape's own fill attribute beats the inherited value Target the shapes, or remove their fill attributes and use currentColor
Labels get tiny on a phone Text is sized in viewBox units, so it shrinks with the drawing Use a larger font size in units, or put labels in HTML beside the SVG
A path heads the wrong way Lower-case (relative) and upper-case (absolute) commands are mixed up Check the case of each letter in d

An SVG chart or badge is easy to break when sent as a file: an .html attachment may open as plain code, and a screenshot loses the hover states and the text screen readers use.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can switch the colours and drag the sliders themselves. If you change the drawing later, the same link shows the new version.

Questions people ask

Do I need the xmlns attribute on an inline svg tag?

Not inside an HTML page. The HTML parser puts <svg> in the SVG namespace on its own. You do need xmlns="http://www.w3.org/2000/svg" when the SVG is saved as a separate .svg file, and document.createElementNS with that namespace when you create SVG elements from JavaScript.

What size is an svg with no width, height or viewBox?

It gets a default box of 300 by 150 CSS pixels. With a viewBox and no width or height, it takes the width of its container and a height from the viewBox proportions, which is why a viewBox alone is usually the better starting point.

What is the difference between polyline and polygon?

Both take a points list. polygon joins the last point back to the first and closes the shape. polyline leaves it open, so it is the one to use for a line chart or a zigzag.

Why does my line not show up?

A line has no inside to fill, and the default stroke is none. Give it a stroke colour and a stroke-width and it appears.

Can CSS change the colour of an SVG loaded with an img tag?

No. An SVG in <img> is drawn as a sealed picture, so the page's CSS and scripts cannot reach its shapes. Paste the markup inline, or see the guide on changing SVG colour for the other options.

Keep reading