The SVG path element in HTML, and SVGPathElement in JavaScript

A path element inside an inline SVG is drawn by its d attribute. In JavaScript the same element is an SVGPathElement, which can measure itself and tell you the point at any distance along it.

A <path> inside an inline <svg> draws any shape you can describe with a d string. In JavaScript that element is an SVGPathElement, a DOM interface (not an event).

Its two most useful methods are getTotalLength(), how long the path is, and getPointAtLength(), where the point at a given distance sits.

Try it. Drag the slider and the dot rides the curve.

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 path element in JavaScript</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  svg { display: block; width: 100%; max-width: 480px; background: #fff; border-radius: 12px; box-shadow: 0 2px 10px rgba(0, 0, 0, .08); }
  #track { fill: none; stroke: #2563eb; stroke-width: 4; stroke-linecap: round; }
  #dot { fill: #f97316; stroke: #fff; stroke-width: 2; }
  label { display: block; margin: 14px 0 6px; font-weight: 600; }
  input[type=range] { width: 100%; max-width: 480px; }
  output { display: block; margin-top: 8px; font: 14px/1.5 ui-monospace, Consolas, monospace; white-space: pre; }
</style>
</head>
<body>
<svg viewBox="0 0 320 160" role="img" aria-label="A curved path with a dot on it">
  <path id="track" d="M20 120 Q90 10 160 80 Q230 150 300 40"/>
  <circle id="dot" r="8" cx="20" cy="120"/>
</svg>

<label for="slider">Distance along the path</label>
<input id="slider" type="range" min="0" max="100" value="0">
<output id="out"></output>

<script>
  const path = document.getElementById('track');   // an SVGPathElement
  const dot = document.getElementById('dot');
  const slider = document.getElementById('slider');
  const out = document.getElementById('out');

  const total = path.getTotalLength();             // length in user units

  function update() {
    const dist = total * slider.value / 100;
    const p = path.getPointAtLength(dist);         // a point object with x and y
    dot.setAttribute('cx', p.x);
    dot.setAttribute('cy', p.y);
    out.textContent = 'length ' + total.toFixed(1) +
      '\ndistance ' + dist.toFixed(1) +
      '\npoint (' + p.x.toFixed(1) + ', ' + p.y.toFixed(1) + ')';
  }

  slider.addEventListener('input', update);
  update();
</script>
</body>
</html>
A path, a dot and a slider. getTotalLength gives the length, getPointAtLength gives the dot position.

The dot is a plain <circle> in the same <svg>. Its cx and cy come straight from the point the path returns, so no scaling maths is needed.

What the path element is

MDN calls <path> the generic element for defining a shape, and every basic shape can be written as a path. The shape lives in one attribute, d, a string of commands and numbers.

The d string on the left, drawn on the right. M starts, L draws a line, Q draws a curve toward a control point, Z closes the shape.
The d string on the left, drawn on the right. M starts, L draws a line, Q draws a curve toward a control point, Z closes the shape.
Command What it does Example
M x y Move to a point without drawing M 20 140
L x y Straight line to a point L 80 50
Q cx cy x y Curve with one control point Q 150 25 200 100
C Curve with two control points six numbers
A Arc of a circle or ellipse seven numbers
Z Line back to the first point no numbers

An uppercase letter uses absolute coordinates and a lowercase letter uses coordinates relative to the last point. For Z the two cases are the same.

If you only need to put a path on a page, SVG in HTML covers the three ways to include one. This guide is about the JavaScript object.

Measure it with getTotalLength

Select the path like any element. The result is already an SVGPathElement, with no cast or wrapper:

const path = document.getElementById('track');
const total = path.getTotalLength();  // a number, in user units

User units are the numbers used in the d string, not screen pixels. A path whose d is empty, or only a single M, has length 0.

Call it once and keep the number. The drawing effect for a line that "writes itself" uses this length too, and SVG stroke-dasharray shows that technique.

Walk along it with getPointAtLength

Pass a distance from 0 to the total length. You get a point object with x and y numbers:

The same distance rule on a half circle: 0 is the start, half the length is the top, the full length is the end.
The same distance rule on a half circle: 0 is the start, half the length is the top, the full length is the end.
const p = path.getPointAtLength(total * 0.5);  // halfway along
dot.setAttribute('cx', p.x);
dot.setAttribute('cy', p.y);

The distance is measured along the curve, so equal steps in the number give equal steps along the line. To animate, change the distance on each frame with requestAnimationFrame and move the dot to the new point.

Keep the dot inside the same <svg> as the path. Then both use the same units and the same viewBox.

Build a path in JavaScript

Setting d is the whole job. Write the string yourself, or join pieces:

path.setAttribute('d', 'M20 120 L' + x + ' ' + y);

Creating a brand-new path is where most people get stuck. SVG elements live in their own namespace, so document.createElement('path') makes an HTML element called path, and the browser ignores it. Use createElementNS with the SVG namespace string.

createElement gives an HTMLUnknownElement and nothing is drawn. createElementNS with the SVG namespace gives an SVGPathElement.
createElement gives an HTMLUnknownElement and nothing is drawn. createElementNS with the SVG namespace gives an SVGPathElement.

Try both and read what the browser reports:

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>createElement vs createElementNS for a path</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  svg { display: block; width: 100%; max-width: 480px; background: #fff; border-radius: 12px; box-shadow: 0 2px 10px rgba(0, 0, 0, .08); }
  .row { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0; }
  button { font: inherit; padding: 9px 12px; border: 0; border-radius: 8px; color: #fff; cursor: pointer; }
  #bad { background: #c2410c; }
  #good { background: #0f5132; }
  #reset { background: #6b7280; }
  pre { margin: 0; padding: 10px 12px; border-radius: 8px; background: #fff; font: 13px/1.5 ui-monospace, Consolas, monospace; white-space: pre-wrap; min-height: 6.5em; }
</style>
</head>
<body>
<svg id="svg" viewBox="0 0 320 120" role="img" aria-label="An empty drawing area">
  <rect width="320" height="120" fill="#fafbfc"/>
</svg>

<div class="row">
  <button id="bad">createElement('path')</button>
  <button id="good">createElementNS(SVG_NS, 'path')</button>
  <button id="reset">Reset</button>
</div>
<pre id="log">Press a button to add a path.</pre>

<script>
  const svg = document.getElementById('svg');
  const log = document.getElementById('log');
  const SVG_NS = 'http://www.w3.org/2000/svg';

  function add(el, color, y) {
    el.setAttribute('d', 'M20 ' + y + ' Q160 ' + (y - 40) + ' 300 ' + y);
    el.setAttribute('fill', 'none');
    el.setAttribute('stroke', color);
    el.setAttribute('stroke-width', '6');
    el.setAttribute('stroke-linecap', 'round');
    svg.appendChild(el);
    log.textContent =
      'constructor: ' + el.constructor.name + '\n' +
      'instanceof SVGPathElement: ' + (el instanceof SVGPathElement) + '\n' +
      'has getTotalLength: ' + (typeof el.getTotalLength === 'function') + '\n' +
      'drawn: ' + (el instanceof SVGPathElement ? 'yes' : 'no, the browser ignores it');
  }

  // wrong: this makes an HTML element named "path"
  document.getElementById('bad').addEventListener('click', () => {
    add(document.createElement('path'), '#c2410c', 50);
  });

  // right: the SVG namespace makes a real SVGPathElement
  document.getElementById('good').addEventListener('click', () => {
    add(document.createElementNS(SVG_NS, 'path'), '#0f5132', 95);
  });

  document.getElementById('reset').addEventListener('click', () => {
    svg.querySelectorAll('path').forEach((p) => p.remove());
    log.textContent = 'Press a button to add a path.';
  });
</script>
</body>
</html>
Press each button. The log shows the element type, and only the second path is drawn.

The namespace string is http://www.w3.org/2000/svg. Keep it in a constant, as the example does, so you type it once.

A finished example: draw a path, then walk it

This version builds the d string from pointer movement. pointerdown starts with M, each pointermove appends an L, and the total length updates after every segment. Press Walk and the dot follows what you drew.

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>Draw a path, then walk along it</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  svg {
    display: block; width: 100%; max-width: 480px; background: #fff; border-radius: 12px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, .08); cursor: crosshair;
    touch-action: none;   /* a finger draws instead of scrolling the page */
  }
  #line { fill: none; stroke: #2563eb; stroke-width: 4; stroke-linecap: round; stroke-linejoin: round; }
  #dot { fill: #f97316; stroke: #fff; stroke-width: 2; }
  .row { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; margin: 12px 0; }
  button { font: inherit; padding: 9px 14px; border: 0; border-radius: 8px; background: #1d4ed8; color: #fff; cursor: pointer; }
  button.alt { background: #6b7280; }
  button:disabled { opacity: .45; cursor: default; }
  #info { font: 14px ui-monospace, Consolas, monospace; }
  .hint { margin: 0; font-size: 14px; color: #4b5563; }
</style>
</head>
<body>
<svg id="pad" viewBox="0 0 320 220" role="img" aria-label="Drawing area">
  <path id="line" d=""/>
  <circle id="dot" r="8" cx="-20" cy="-20"/>
</svg>

<div class="row">
  <button id="walk" disabled>Walk</button>
  <button id="clear" class="alt">Clear</button>
  <span id="info">length 0</span>
</div>
<p class="hint">Drag inside the box to draw, then press Walk.</p>

<script>
  const pad = document.getElementById('pad');
  const line = document.getElementById('line');
  const dot = document.getElementById('dot');
  const walk = document.getElementById('walk');
  const info = document.getElementById('info');

  let d = '', last = null, drawing = false, raf = 0;

  // turn a pointer position into SVG user units (the viewBox is 320 wide)
  function toSvg(e) {
    const r = pad.getBoundingClientRect();
    const k = 320 / r.width;
    return { x: (e.clientX - r.left) * k, y: (e.clientY - r.top) * k };
  }

  function refresh() {
    line.setAttribute('d', d);
    const len = line.getTotalLength();
    info.textContent = 'length ' + len.toFixed(0);
    walk.disabled = len === 0;
  }

  pad.addEventListener('pointerdown', (e) => {
    cancelAnimationFrame(raf);
    pad.setPointerCapture(e.pointerId);
    drawing = true;
    last = toSvg(e);
    d = 'M' + last.x.toFixed(1) + ' ' + last.y.toFixed(1);   // start a new path
    dot.setAttribute('cx', -20);
    refresh();
  });

  pad.addEventListener('pointermove', (e) => {
    if (!drawing) return;
    const p = toSvg(e);
    if (Math.hypot(p.x - last.x, p.y - last.y) < 3) return;  // skip tiny moves
    d += ' L' + p.x.toFixed(1) + ' ' + p.y.toFixed(1);
    last = p;
    refresh();
  });

  pad.addEventListener('lostpointercapture', () => { drawing = false; });

  walk.addEventListener('click', () => {
    cancelAnimationFrame(raf);
    const len = line.getTotalLength();
    const speed = 160;                       // user units per second
    const t0 = performance.now();
    (function step(now) {
      const dist = Math.min(len, (now - t0) / 1000 * speed);
      const p = line.getPointAtLength(dist); // where the dot goes
      dot.setAttribute('cx', p.x);
      dot.setAttribute('cy', p.y);
      if (dist < len) raf = requestAnimationFrame(step);
    })(t0);
  });

  document.getElementById('clear').addEventListener('click', () => {
    cancelAnimationFrame(raf);
    d = '';
    dot.setAttribute('cx', -20);
    refresh();
  });
</script>
</body>
</html>
Draw inside the box with a mouse or a finger. Walk sends a dot along the line at a steady speed.

Three details make it work on a phone and on a scaled drawing:

  • Convert coordinates. The pointer gives screen pixels. Divide by the box width and multiply by the viewBox width to get user units.
  • Stop the page scrolling. touch-action: none on the <svg> lets a finger draw. Pointer events explains the rest of that code.
  • Handle length 0. The Walk button stays disabled until the path has a segment.

What else the path object offers

SVGPathElement inherits more from SVGGeometryElement. isPointInFill() and isPointInStroke() test whether a point lies inside the fill or on the stroke, using the element's pointer-events value. pathLength is a read-only property that reflects the pathLength attribute.

The old createSVGPathSeg methods were removed in SVG 2, so build paths by writing the d string. MDN lists getTotalLength() and getPointAtLength() as widely available across browsers.

When it does not work

What you see Cause Fix
getTotalLength is not a function The variable is not an SVG path, for example null or a <div> Check the id and that the script runs after the element exists
A path made in JavaScript is invisible Made with createElement, so it is an HTML element Use createElementNS with the SVG namespace
The length is 0, or an error says the path is empty d is empty or has only an M Add at least one L, Q or other segment
The dot is not on the line The dot is in another element or another coordinate system Put the dot in the same <svg> and set cx and cy
Nothing shows even with a valid d The path has no stroke and its fill is none Set stroke and stroke-width
Drawing stops on a phone The finger is scrolling the page touch-action: none on the <svg>
createSVGPathSeg is missing Those methods were removed in SVG 2 Set the d attribute instead

A path you can measure and walk is better seen than described. A screenshot cannot move the dot, and an .html attachment may open as plain code on a phone. Opening an HTML file on a phone covers why.

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 draw and press Walk themselves. If you change the code later, the same link shows the new version.

Questions people ask

What is SVGPathElement?

It is the JavaScript interface that corresponds to the SVG path element. Any path you select from the page, for example with getElementById, is an object of that type. It inherits from SVGGeometryElement, which supplies getTotalLength() and getPointAtLength().

How do I get the length of an SVG path in JavaScript?

Call getTotalLength() on the path element. It returns a number in user units, the same units the d attribute is written in. It does not need any argument.

How do I find a point along an SVG path?

Call getPointAtLength(distance) with a number from 0 to the total length. It returns a point object, so read its x and y properties and use them for a circle, a label or any other SVG element in the same drawing.

Why does document.createElement('path') draw nothing?

It creates an HTML element named path, which the browser treats as an unknown element. SVG elements need the SVG namespace, so call document.createElementNS('http://www.w3.org/2000/svg', 'path') instead.

Is there an svgpathelement event?

No. SVGPathElement is an interface, not an event name. A path is still an event target like any element, so addEventListener('click', ...) and pointer events work on it.

Keep reading