Firefox 137 just shipped the SVG Path Data API — a native way to read, construct, and manipulate SVG paths programmatically in JavaScript. Learn getPathData(), setPathData(), and getPathSegmentAtLength() with real code examples and practical use cases.
For over two decades, manipulating SVG paths in JavaScript meant one thing: parsing
and constructing strings. Every <path d="M10 10 L 20 20...">
required you to split the d attribute, tokenise commands, handle
implicit commands, and then reassemble everything — all through string concatenation.
It was fragile, error-prone, and completely opaque to type checking.
The SVG Path Data API (shipped in Firefox 137+,
W3C Editor's Draft September 2025) changes this completely. It introduces three
methods on SVGPathElement — getPathData(),
setPathData(), and getPathSegmentAtLength() — that expose
SVG path data as structured JavaScript objects instead of opaque strings.
In this guide, I'll walk you through everything you need to know: the API surface with full syntax, practical code examples, real-world use cases, browser support, polyfill strategies, and the pitfalls to avoid.
Before the Path Data API, any SVG path manipulation involved working with the
d attribute — a string like
"M 10 10 L 20 20 L 30 10 Z". To modify a path programmatically,
you had to:
element.getAttribute('d') to get the raw stringelement.setAttribute('d', newString)Any mistake in the parser — a missed implicit command, wrong number of bezier control points, or a missing space before a negative number — would silently break the entire SVG rendering. Debugging meant staring at long strings of numbers. This is a solved-class-of-problem that should have had a native API years ago.
💡 The Takeaway: String-based SVG path manipulation is error-prone, hard to debug, and completely unsuitable for complex path operations like morphing animations or interactive editors. The Path Data API fixes this with structured, typed data.
The SVG Path Data API adds three methods to SVGPathElement, each
solving a specific need in the path manipulation workflow:
| Method | Purpose | Returns |
|---|---|---|
getPathData() |
Read all path segments as structured objects | PathDataSegment[] |
setPathData(segments) |
Replace path with a new array of segments | undefined |
getPathSegmentAtLength(distance) |
Find which segment is at a given length along the path | PathDataSegment | null |
Every segment returned by the API follows the same structure:
interface PathDataSegment {
type: string; // Command letter: 'M', 'L', 'C', 'Q', 'A', 'Z', etc.
values: number[]; // Coordinates for this command
}
The type property uses the same one or two-letter command codes
as the SVG path string syntax: M (moveto), L (lineto),
C (cubic bezier curve), Q (quadratic bezier),
A (arc), Z (closepath). Lowercase means relative
coordinates; uppercase means absolute.
The values array contains exactly the right number of coordinates
for the command type:
| Command | Type | Values | Description |
|---|---|---|---|
| M, m | Moveto | [x, y] |
Move to point |
| L, l | Lineto | [x, y] |
Draw a straight line |
| H, h | Horizontal lineto | [x] |
Horizontal line |
| V, v | Vertical lineto | [y] |
Vertical line |
| C, c | Cubic bezier | [x1, y1, x2, y2, x, y] |
Two control points + end point |
| S, s | Smooth cubic bezier | [x2, y2, x, y] |
Reflected control point + end point |
| Q, q | Quadratic bezier | [x1, y1, x, y] |
One control point + end point |
| T, t | Smooth quadratic bezier | [x, y] |
Reflected control point + end point |
| A, a | Arc | [rx, ry, xAxisRot, largeArcFlag, sweepFlag, x, y] |
Elliptical arc segment |
| Z, z | Closepath | [] |
Close the current subpath |
The getPathData() method is the simplest of the three. Call it on
any <path> element and you get a clean array of segment objects:
const path = document.querySelector('svg path');
// The old way — fragile string parsing
const oldD = path.getAttribute('d');
// "M10 80 Q 52.5 10, 95 80 T 180 80 Z"
// The new way — structured data
const segments = path.getPathData();
console.log(segments);
// [
// { type: 'M', values: [10, 80] },
// { type: 'Q', values: [52.5, 10, 95, 80] },
// { type: 'T', values: [180, 80] },
// { type: 'Z', values: [] }
// ]
Each segment is a clean object with explicit type and values. No more guessing
whether "10-20" means [10, -20] or [10, 20].
No more writing your own SVG path tokeniser.
With structured data, analysing a path is trivial:
function analyzePath(pathElement) {
const segments = pathElement.getPathData();
return {
totalSegments: segments.length,
commandTypes: segments.reduce((acc, s) => {
acc[s.type] = (acc[s.type] || 0) + 1;
return acc;
}, {}),
boundingBox: calculateBoundingBox(segments),
isClosed: segments[segments.length - 1]?.type === 'Z'
};
}
// Get the bounding box from segment coordinates
function calculateBoundingBox(segments) {
let minX = Infinity, minY = Infinity;
let maxX = -Infinity, maxY = -Infinity;
for (const seg of segments) {
const { type, values } = seg;
if (type === 'Z' || type === 'z') continue;
// Extract end-point coordinates based on command type
const endIdx = getEndCoordinateIndex(type);
if (endIdx >= 0) {
const x = values[endIdx - 1];
const y = values[endIdx];
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
}
return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
}
function getEndCoordinateIndex(type) {
const ends = { M: 2, L: 2, H: 1, V: 1, C: 6, S: 4, Q: 4, T: 2, A: 6 };
return ends[type.toUpperCase()] - 1;
}
Before this API, extracting bounding box information from SVG paths required
parsing the d string with regex — and every regex was a fragile
approximation. Now it's a straightforward array walk.
The real power of the API comes with setPathData(). Instead of
building a string, you pass an array of PathDataSegment objects
to update the path's geometry:
const path = document.querySelector('svg path');
// Read existing path
const segments = path.getPathData();
// Modify the second segment — move its end point
segments[1] = {
type: 'L',
values: [150, 200]
};
// Write back — no string concatenation needed
path.setPathData(segments);
This is vastly safer than string manipulation. Each segment is an isolated object. Changing one segment can't accidentally corrupt the rest of the path — unlike string concatenation where a missing space or extra character breaks everything.
You can also build paths entirely in JavaScript without touching a string:
function createStarPath(cx, cy, outerR, innerR, points) {
const segments = [];
const step = Math.PI / points;
for (let i = 0; i < points * 2; i++) {
const r = i % 2 === 0 ? outerR : innerR;
const angle = i * step - Math.PI / 2;
const x = cx + r * Math.cos(angle);
const y = cy + r * Math.sin(angle);
if (i === 0) {
segments.push({ type: 'M', values: [x, y] });
} else {
segments.push({ type: 'L', values: [x, y] });
}
}
segments.push({ type: 'Z', values: [] });
return segments;
}
// Create a 5-pointed star
const starSegments = createStarPath(100, 100, 80, 40, 5);
pathElement.setPathData(starSegments);
This function generates a perfectly scaled star path using only structured data. No string building, no escaping, no edge cases with negative numbers — just clean arithmetic and object construction.
A common use case is scaling or translating path coordinates. With the old API, you had to parse the string, extract all coordinates, modify them, and rebuild the string. With the Path Data API:
function scalePath(pathElement, scaleX, scaleY) {
const segments = pathElement.getPathData();
for (const seg of segments) {
if (seg.type === 'Z' || seg.type === 'z') continue;
for (let i = 0; i < seg.values.length; i++) {
if (i % 2 === 0) {
seg.values[i] *= scaleX; // X coordinate
} else {
seg.values[i] *= scaleY; // Y coordinate
}
}
}
pathElement.setPathData(segments);
}
// Scale a path by 1.5x horizontally
scalePath(myPath, 1.5, 1);
The alternating X/Y pattern in the values array follows SVG
convention: even indices (0, 2, 4...) are X coordinates, odd indices
(1, 3, 5...) are Y coordinates. This holds for all command types.
One of the most exciting use cases is path morphing — smoothly animating from one shape to another. The Path Data API makes this dramatically simpler because both the source and target shapes are arrays of structured objects with the same format:
function morphPath(pathElement, targetSegments, duration = 1000) {
const startSegments = pathElement.getPathData();
// Validate segment count matches
if (startSegments.length !== targetSegments.length) {
console.warn('Segment count mismatch — morphing may look wrong');
}
const startTime = performance.now();
function animate(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = easeInOutCubic(progress);
const currentSegments = startSegments.map((start, i) => {
if (i >= targetSegments.length) return start;
const target = targetSegments[i];
const values = start.values.map((v, j) => {
const targetVal = target.values[j] ?? v;
return v + (targetVal - v) * eased;
});
return { type: target.type, values };
});
pathElement.setPathData(currentSegments);
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
}
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}
// Usage: morph a square into a circle
const square = [
{ type: 'M', values: [50, 50] },
{ type: 'L', values: [150, 50] },
{ type: 'L', values: [150, 150] },
{ type: 'L', values: [50, 150] },
{ type: 'Z', values: [] }
];
const circle = [
{ type: 'M', values: [100, 50] },
{ type: 'C', values: [127.6, 50, 150, 72.4, 150, 100] },
{ type: 'C', values: [150, 127.6, 127.6, 150, 100, 150] },
{ type: 'C', values: [72.4, 150, 50, 127.6, 50, 100] },
{ type: 'C', values: [50, 72.4, 72.4, 50, 100, 50] },
{ type: 'Z', values: [] }
];
morphPath(myPath, circle, 2000);
💡 Key Insight: For smooth morphing, both paths should have the same number of segments and the same command types at corresponding positions. When this isn't possible, add intermediate steps or normalise the segments before morphing. Always validate segment counts.
The third method, getPathSegmentAtLength(distance), answers a
question that previously required complex geometry libraries: "What segment
of the path is at this distance from the start?"
const path = document.querySelector('svg path');
const totalLength = path.getTotalLength();
// Find which segment is at 50% of the path length
const midpoint = path.getPathSegmentAtLength(totalLength / 2);
console.log(midpoint);
// { type: 'C', values: [x1, y1, x2, y2, x, y] }
This is invaluable for:
Here's a practical example that draws a path segment by segment:
async function progressiveDraw(pathElement, durationPerSegment = 300) {
const segments = pathElement.getPathData();
// Start with just the first moveto
const partialSegments = [segments[0]];
pathElement.setPathData(partialSegments);
for (let i = 1; i < segments.length; i++) {
if (segments[i].type === 'Z' || segments[i].type === 'z') {
partialSegments.push(segments[i]);
pathElement.setPathData(partialSegments);
break;
}
partialSegments.push(segments[i]);
pathElement.setPathData(partialSegments);
await new Promise(r => setTimeout(r, durationPerSegment));
}
}
Each segment is added incrementally, creating an animated drawing effect.
Combined with getPathSegmentAtLength(), you could also tie the
segment reveal to a scroll position or a scrub control.
The Path Data API is a natural fit for programmatic charting and data visualisation. Here's a simple line chart generator:
function createLineChart(dataPoints, width, height, padding = 20) {
if (dataPoints.length < 2) return [];
const xScale = (width - 2 * padding) / (dataPoints.length - 1);
const yMin = Math.min(...dataPoints);
const yMax = Math.max(...dataPoints);
const yRange = yMax - yMin || 1;
const segments = [];
for (let i = 0; i < dataPoints.length; i++) {
const x = padding + i * xScale;
const y = height - padding - ((dataPoints[i] - yMin) / yRange) * (height - 2 * padding);
if (i === 0) {
segments.push({ type: 'M', values: [x, y] });
} else {
// Use smooth curves instead of straight lines
const prevX = padding + (i - 1) * xScale;
const prevY = height - padding - ((dataPoints[i - 1] - yMin) / yRange) * (height - 2 * padding);
const cpx1 = prevX + xScale * 0.5;
const cpx2 = x - xScale * 0.5;
segments.push({
type: 'C',
values: [cpx1, prevY, cpx2, y, x, y]
});
}
}
return segments;
}
// Usage
const data = [10, 45, 30, 70, 55, 90, 85];
const chartSegments = createLineChart(data, 400, 200);
chartPath.setPathData(chartSegments);
This generates a smooth bezier curve chart from raw data. The cubic bezier control points create natural-looking curves between data points. With the old string-based approach, generating the command string for something like this required careful concatenation and was prone to missing commas or spaces.
The most ambitious use case is an interactive SVG path editor. With
getPathData() and setPathData(), you can build
a drag-to-modify path editor in surprisingly few lines:
class PathEditor {
constructor(pathElement) {
this.path = pathElement;
this.segments = pathElement.getPathData();
this.dragging = null;
this.bindEvents();
}
getPointPositions() {
// Extract all endpoint coordinates as manipulable points
const points = [];
for (const seg of this.segments) {
if (seg.type === 'Z' || seg.type === 'z') continue;
const endIdx = [6, 4, 2].find(i => seg.values.length >= i) ?? 2;
if (endIdx >= 2) {
points.push({
segIndex: this.segments.indexOf(seg),
valIndexX: endIdx - 2,
valIndexY: endIdx - 1,
x: seg.values[endIdx - 2],
y: seg.values[endIdx - 1]
});
}
}
return points;
}
bindEvents() {
const svg = this.path.closest('svg');
svg.addEventListener('mousedown', (e) => {
const rect = svg.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
// Find closest point to click
const points = this.getPointPositions();
let minDist = Infinity;
for (const p of points) {
const dist = Math.hypot(p.x - mx, p.y - my);
if (dist < minDist && dist < 10) {
minDist = dist;
this.dragging = p;
}
}
});
svg.addEventListener('mousemove', (e) => {
if (!this.dragging) return;
const rect = svg.getBoundingClientRect();
const seg = this.segments[this.dragging.segIndex];
if (seg) {
seg.values[this.dragging.valIndexX] = e.clientX - rect.left;
seg.values[this.dragging.valIndexY] = e.clientY - rect.top;
this.path.setPathData(this.segments);
}
});
svg.addEventListener('mouseup', () => {
this.dragging = null;
});
}
}
// Usage
const editor = new PathEditor(document.querySelector('svg path'));
This is a fully functional path editor in under 60 lines. The key insight is
that setPathData() accepts the same format as
getPathData() returns — so the drag handler simply modifies
the values array and writes it back. With the old string API, each drag
event would require a full path string rebuild.
As of June 2026, the SVG Path Data API has limited browser support:
Always check for support before using the API:
function supportsPathDataAPI() {
const path = document.createElementNS(
'http://www.w3.org/2000/svg', 'path'
);
return 'getPathData' in path;
}
if (supportsPathDataAPI()) {
// Use the native Path Data API
const segments = myPath.getPathData();
} else {
// Fallback: parse d-attribute manually
const dString = myPath.getAttribute('d');
const segments = parsePathString(dString);
}
For production use, implement a polyfill that wraps the old string API.
A basic polyfill can parse the d attribute into segments for
reading and convert segments back to a string for writing:
// Minimal polyfill concept — expand for production use
function polyfillPathData() {
if (supportsPathDataAPI()) return;
SVGPathElement.prototype.getPathData = function() {
return parseDString(this.getAttribute('d') || '');
};
SVGPathElement.prototype.setPathData = function(segments) {
this.setAttribute('d', segmentsToDString(segments));
};
}
function segmentsToDString(segments) {
return segments.map(s => {
if (s.type === 'Z' || s.type === 'z') return 'Z';
return s.type + ' ' + s.values.join(' ');
}).join(' ');
}
// Apply polyfill before using the API
polyfillPathData();
⚠️ Production Note: The polyfill above is simplified for illustration. A production polyfill needs to handle implicit repeated commands (e.g., "M 10 10 L 20 20 30 30"), arc flags (which are not separated by commas in the string format), and edge cases like scientific notation. Consider using a community-maintained polyfill for production.
The SVG Path Data API is powerful, but it's important to understand its current limitations:
setPathData() call
triggers a full re-render of the path. For high-frequency animations (60fps
path morphing), this is fine. For paths with thousands of segments, test
performance before committing.setPathData() does not validate
that the segment values are sensible. If you pass NaN, Infinity, or negative
arc radii, the path may render incorrectly or invisibly.The SVG Path Data API is a long-overdue addition to the web platform that finally brings structured data manipulation to SVG paths. It replaces two decades of fragile string-parsing workarounds with clean, typed JavaScript objects.
The key takeaways:
getPathData() reads path segments as objects with
type and values propertiessetPathData() writes path data from arrays of these objects
— no string manipulation requiredgetPathSegmentAtLength() returns the segment at a given
distance along the pathStart experimenting with the SVG Path Data API today. Even with current browser limitations, the API is stable enough for experimental projects, and the polyfill approach works well for production use while other browsers catch up.
Want to build interactive SVG visualisations? I'm available for frontend architecture, SVG/Canvas development, and data visualisation consulting. View my services or contact me directly.
I build data-driven web applications with SVG, Canvas, and modern JavaScript. Let's discuss your project — free consultation.