🛹 Typescript tools for advanced processing of SVG path data.
See the codeA modern set of Typescript tools for manipulating the d (description) attribute for SVGPathElement items. The library is implementing modern JavaScript API to produce reusable path strings with lossless quality. In addition, you also have a powerful tool to convert other SVG shapes like <circle> or <rect> to <path>.
While you may find familiar tools inside, this library brings new additions:
getBBox, getPointAtLength and getTotalLength are more reliable and much more accurate than the native methods, not to mention their high performance ratings;getPropertiesAtLength, getSegmentOfPoint or isPointInStroke;The key differences with other libraries:
SVGPathCommander can use the DOMMatrix API for SVGPathElement path command transformation and implements a very fast and modernized DOMMatrix shim. There are a couple of good reasons for this implementation:
This library is available on CDN, npm and JSR.
A comparison of SVGPathCommander with the SvgPath library v2.6.0 and its svg-path-bbox helper v2.1.0, run with pnpm bench against a sample path with 11650 path segments. All values are in milliseconds, reported as avg / best over 10 runs after 3 warmups; lower is better. The vs svgpath column is the avg ratio of SVGPathCommander to svgpath.
transform2d uses { scale: 0.8 }, transform3d uses { rotate: [15, 15, 30] }. SvgPath only supports 2D transforms, so transform3d is marked n/a for it.
| Operation | SVGPathCommander (static) | svgpath | vs svgpath |
|---|---|---|---|
| parse | 7.4 / 7 | 9.7 / 9 | 0.8x |
| bbox | 5.2 / 4 | 6.8 / 6 | 0.8x |
| transform2d | 1.7 / 1 | 1.2 / 1 | 1.4x |
| transform3d | 14.3 / 11 | n/a | n/a |
| string | 6.2 / 5 | 5 / 5 | 1.2x |
| Operation | SVGPathCommander (instance) | svgpath | vs svgpath |
|---|---|---|---|
| parse | 7.3 / 7 | 9.7 / 9 | 0.8x |
| bbox | 5 / 4 | 6.8 / 6 | 0.7x |
| transform2d | 1.3 / 0 | 1.2 / 1 | 1.1x |
| transform3d | 8.4 / 7 | n/a | n/a |
| string | 5.9 / 5 | 5 / 5 | 1.2x |
Last benchmark run on 2026-08-05. Generated by scripts/bench.js; update with pnpm bench.
npm install svg-path-commander
# or pnpm add svg-path-commander
# or bun add svg-path-commander
# or deno add npm:svg-path-commander
# or deno add jsr:@thednp/svg-path-commander
# or npx jsr add @thednp/svg-path-commander
In a Deno project, install from JSR and import the raw TypeScript source directly:
import SVGPathCommander from "jsr:@thednp/svg-path-commander";
Version 2.2.0+
<script src="https://cdn.jsdelivr.net/npm/svg-path-commander/dist/index.min.js">
Versions before 2.2.0
<script src="https://cdn.jsdelivr.net/npm/svg-path-commander/dist/svg-path-commander.js">
Flip a path on the X axis:
import SVGPathCommander from 'svg-path-commander';
const path = 'M0 0L100 0L50 100';
const flippedPathString = new SVGPathCommander(path).flipX().toString();
// result => 'M0 100h100L50 0'
Optimize a path string by using the round option, to round numbers to 2 decimals and finding shorthand where possible:
const optimizedPathString = new SVGPathCommander(path, {round: 2}).optimize().toString();
Or why not apply a 2D transformation and even a 3D transformation:
// a transform object
const transform = {
translate: 15, // X axis translation
rotate: 15, // Z axis rotation
scale: 0.75, // uniform scale on X, Y, Z axis
skew: 15, // skew 15deg on the X axis
origin: [15, 0] // if not specified, it will use the default origin value [0, 0]
}
const transformed2DPathString = new SVGPathCommander(path).transform(transform).toString();
// apply a 3D transformation
const transform = {
translate: [15, 15, 15], // `[15, 15]` would apply a 2D translation, and only `15` for X axis translation
rotate: [15, 15, 15], // or only "15" for 2D rotation on Z axis
scale: [0.7, 0.75, 0.8], // or only "0.7" for 2D scale on all X, Y, Z axis
skew: [15, 15], // or only "15" for the X axis
origin: [15, 15, 15] // full `transform-origin` for a typical 3D transformation
}
const transformed3DPathString = new SVGPathCommander(path).transform(transform).toString();
Access the bbox instance property to apply a consistent transform-origin:
// apply a 3D transformation with a consistent origin
const transformed3DPath = new SVGPathCommander(path);
const { cx, cy, cz } = transformed3DPath.bbox;
const transform = {
translate: [15, 15, 15], // `[15, 15]` would apply a 2D translation, and only `15` for X axis translation
rotate: [15, 15, 15], // or only "15" for 2D rotation on Z axis
scale: [0.7, 0.75, 0.8], // or only "0.7" for 2D scale on all X, Y, Z axis
skew: [15, 15], // or only "15" for the X axis
origin: [cx, cy, cz] // the origin
}
const transformed3DPathString = transformed3DPath.transform(transform).toString();
SVGPathCommander comes with a full range of additional static methods, here's how to normalize a path:
const path = 'M0 0 H50';
const normalizedPath = SVGPathCommander.normalizePath(path);
// result => [['M', 0, 0], ['L', 50, 0]]
Reverse a path:
const path = SVGPathCommander.parsePathString('M0 0 H50');
const reversedPath = SVGPathCommander.reversePath(path);
// result => [['M', 50, 0], ['H', 0]]
Export to string:
const myPathString = SVGPathCommander.pathToString([['M', 0, 0], ['L', 50, 0]]);
// result => 'M0 0 L50 0'
Check a path string validity:
SVGPathCommander.isValidPath(path);
// result => boolean
Check if path is a certain kind of PathArray:
SVGPathCommander.isAbsoluteArray([['M', 0, 0], ['L', 50, 0]]);
// result => true
Use treeshake and create a custom function to apply a 3D transformation using static methods:
import { parsePathString, getPathBBox, transformPath, pathToString } from 'svg-path-commander';
function myTransformFn(pathInput: string | PathArray, transformObject: TransformObject) {
const path = parsePathString(pathInput);
const { cx, cy, cz } = getPathBBox(path);
return pathToString(
transformPath(path, {
...transformObject, origin: [cx, cy, cz]
})
)
}
In extreme cases where performance is paramount, you can consider the parent SVG viewBox attribute to extract a bounding box required for a consistent transform origin.
// const svgViewBox = document.getElementById('my-svg').getAttribute('viewBox');
const viewBox = '0 0 24 24';
const [x, y, width, height] = viewBox.split(/\s/).map(Number);
const origin = [
x + width / 2, // CX
y + height / 2, // CY
Math.max(width, height) + Math.min(width, height) / 2, // CZ
];
// use this origin for your shape transformation
const myNewString = new SVGPathCommander('M0 0 H50')
.transform({ rotate: [35, 0, 0], origin })
.toString();
Convert a shape to <path> and transfer all non-specific attributes
const myCircle = document.getElementById('myCircle');
SVGPathCommander.shapeToPath(myCircle, true);
Alternatively you can create <path> from specific attributes:
const myRectAttr = {
type: 'rect',
x: 25,
y: 25,
width: 50,
height: 50,
rx: 5
};
const myRectPath = SVGPathCommander.shapeToPath(myRectAttr);
document.getElementById('mySVG').append(myRectPath);
Server-side using jsdom:
const { document } = new JSDOM(
`<html>
<head></head>
<body>
<svg id="mySVG" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect id="myRect" x="0" width="100" height="100" rx="15" />
</svg>
</body>
</html>`,
{
pretendToBeVisual: true,
}
).window;
const myRect = document.getElementById('myRect');
SVGPathCommander.shapeToPath(myRect, true, document);
Get the path length:
const myPathLength = SVGPathCommander.getTotalLength('M0 0L50 0L25 50z');
// result => 161.80339887498948
Get a point along the path:
const myPoint = SVGPathCommander.getPointAtLength('M0 0L50 0L25 50z', 85);
// result => {x: 34.34752415750147, y: 31.304951684997057}
Get the path bounding box:
const myPathBBox = SVGPathCommander.getPathBBox('M0 0L50 0L25 50z');
// result => {width: 50, height: 50, x: 0, y: 0, x2: 50, y2: 50, cx: 25, cy: 25, cz: 75}
Find intersection points between two paths:
const count = SVGPathCommander.pathsIntersection(
'M0 50C0 0,100 0,100 50',
'M50 0C100 0,100 100,50 100',
true
);
// result => 1
const points = SVGPathCommander.pathsIntersection(
'M0 50C0 0,100 0,100 50',
'M50 0C100 0,100 100,50 100',
false
);
// result => [{ x: 50, y: 25, t1: 0.5, t2: 0.5 }]
Equalize two paths for morphing (single subpath):
const [eq1, eq2] = SVGPathCommander.equalizeSegments(
'M0 0L100 0L50 100Z',
'M0 0L100 0L100 100L0 100Z'
);
// eq1.length === eq2.length — ready for morphing
Equalize two paths for morphing (single or multi subpath):
const [eq1, eq2] = SVGPathCommander.equalizePaths(
'M0 0L100 0L50 100Z',
'M0 0L100 0L100 100L0 100Z',
{ close: true }
);
// Both paths now have the same number of segments
For developer guidelines, and a complete list of static methods, head over to the wiki/ folder in this repository.
optimize() instance method will not merge path segments (for instance two or more cubic-bezier segments into one or more arc segments); however, the script will try to provide shorthand notations where possible, pick the shortest string for each segment, and generally try to deliver the best possible outcome;pathToString, optimizePath and especially roundPath will always round values to the default of 4 decimals; EG: 0.56676 => 0.567, 0.50 => 0.5; you can change the default option with SVGPathCommander.options.round = 2 or remove the value rounding all together with SVGPathCommander.options.round = false; you can also control this feature via instance options;getSVGMatrix utility we developed will always compute the matrix by applying the transform functions in the following order: translate, rotate, skew and scale, which is the default composition/recomposition order specified in the W3C draft;A (arc) path commands to C (cubic bezier) due to the lack of resources;A segments to C segments is the shipped behavior, a validated arc-preserving alternative exists: an affine-transformed elliptical arc is still an exact elliptical arc, so A segments can be transformed directly via the singular value decomposition of the transformed ellipse shape matrix (radii and rotation from the eigenvalues/eigenvectors of the Gram matrix, sweep flag flipped for negative-determinant matrices). This works for any 2D matrix, and even for 3D matrices whenever the projection is orthographic (default origin: [0,0,0] — the z = 0 plane is untouched by the matrix's z rows/columns, so only the 2x2 submatrix matters); the conversion to C is only mathematically required for perspective projections (non-zero origin z value or a matrix with perspective), where the arc image is a general conic. See experiments/transformArc-tested.ts for the implementation and rationale;R (catmulRomBezier), O, U (ellipse and shorthand ellipse) are not present in the current draft and are not supported;SVGPathElement.getTotalLength() or SVGPathElement.getPointAtLength(), the output of our static methods is within a [0.002 - 0.05] margin delta, but from our experience it's proven to be a more consistent outcome.SVGPathCommander is released under MIT Licence.
Overall
Maintained
Code review
Latest release
Releases this year
4,648 followers · starred Oct 2022
258 followers · starred Mar 2023
67 followers · starred Jun 2024
36 followers · starred May 2023
🛹 Typescript tools for advanced processing of SVG path data.
See the codeA modern set of Typescript tools for manipulating the d (description) attribute for SVGPathElement items. The library is implementing modern JavaScript API to produce reusable path strings with lossless quality. In addition, you also have a powerful tool to convert other SVG shapes like <circle> or <rect> to <path>.
While you may find familiar tools inside, this library brings new additions:
getBBox, getPointAtLength and getTotalLength are more reliable and much more accurate than the native methods, not to mention their high performance ratings;getPropertiesAtLength, getSegmentOfPoint or isPointInStroke;The key differences with other libraries:
SVGPathCommander can use the DOMMatrix API for SVGPathElement path command transformation and implements a very fast and modernized DOMMatrix shim. There are a couple of good reasons for this implementation:
This library is available on CDN, npm and JSR.
A comparison of SVGPathCommander with the SvgPath library v2.6.0 and its svg-path-bbox helper v2.1.0, run with pnpm bench against a sample path with 11650 path segments. All values are in milliseconds, reported as avg / best over 10 runs after 3 warmups; lower is better. The vs svgpath column is the avg ratio of SVGPathCommander to svgpath.
transform2d uses { scale: 0.8 }, transform3d uses { rotate: [15, 15, 30] }. SvgPath only supports 2D transforms, so transform3d is marked n/a for it.
| Operation | SVGPathCommander (static) | svgpath | vs svgpath |
|---|---|---|---|
| parse | 7.4 / 7 | 9.7 / 9 | 0.8x |
| bbox | 5.2 / 4 | 6.8 / 6 | 0.8x |
| transform2d | 1.7 / 1 | 1.2 / 1 | 1.4x |
| transform3d | 14.3 / 11 | n/a | n/a |
| string | 6.2 / 5 | 5 / 5 | 1.2x |
| Operation | SVGPathCommander (instance) | svgpath | vs svgpath |
|---|---|---|---|
| parse | 7.3 / 7 | 9.7 / 9 | 0.8x |
| bbox | 5 / 4 | 6.8 / 6 | 0.7x |
| transform2d | 1.3 / 0 | 1.2 / 1 | 1.1x |
| transform3d | 8.4 / 7 | n/a | n/a |
| string | 5.9 / 5 | 5 / 5 | 1.2x |
Last benchmark run on 2026-08-05. Generated by scripts/bench.js; update with pnpm bench.
npm install svg-path-commander
# or pnpm add svg-path-commander
# or bun add svg-path-commander
# or deno add npm:svg-path-commander
# or deno add jsr:@thednp/svg-path-commander
# or npx jsr add @thednp/svg-path-commander
In a Deno project, install from JSR and import the raw TypeScript source directly:
import SVGPathCommander from "jsr:@thednp/svg-path-commander";
Version 2.2.0+
<script src="https://cdn.jsdelivr.net/npm/svg-path-commander/dist/index.min.js">
Versions before 2.2.0
<script src="https://cdn.jsdelivr.net/npm/svg-path-commander/dist/svg-path-commander.js">
Flip a path on the X axis:
import SVGPathCommander from 'svg-path-commander';
const path = 'M0 0L100 0L50 100';
const flippedPathString = new SVGPathCommander(path).flipX().toString();
// result => 'M0 100h100L50 0'
Optimize a path string by using the round option, to round numbers to 2 decimals and finding shorthand where possible:
const optimizedPathString = new SVGPathCommander(path, {round: 2}).optimize().toString();
Or why not apply a 2D transformation and even a 3D transformation:
// a transform object
const transform = {
translate: 15, // X axis translation
rotate: 15, // Z axis rotation
scale: 0.75, // uniform scale on X, Y, Z axis
skew: 15, // skew 15deg on the X axis
origin: [15, 0] // if not specified, it will use the default origin value [0, 0]
}
const transformed2DPathString = new SVGPathCommander(path).transform(transform).toString();
// apply a 3D transformation
const transform = {
translate: [15, 15, 15], // `[15, 15]` would apply a 2D translation, and only `15` for X axis translation
rotate: [15, 15, 15], // or only "15" for 2D rotation on Z axis
scale: [0.7, 0.75, 0.8], // or only "0.7" for 2D scale on all X, Y, Z axis
skew: [15, 15], // or only "15" for the X axis
origin: [15, 15, 15] // full `transform-origin` for a typical 3D transformation
}
const transformed3DPathString = new SVGPathCommander(path).transform(transform).toString();
Access the bbox instance property to apply a consistent transform-origin:
// apply a 3D transformation with a consistent origin
const transformed3DPath = new SVGPathCommander(path);
const { cx, cy, cz } = transformed3DPath.bbox;
const transform = {
translate: [15, 15, 15], // `[15, 15]` would apply a 2D translation, and only `15` for X axis translation
rotate: [15, 15, 15], // or only "15" for 2D rotation on Z axis
scale: [0.7, 0.75, 0.8], // or only "0.7" for 2D scale on all X, Y, Z axis
skew: [15, 15], // or only "15" for the X axis
origin: [cx, cy, cz] // the origin
}
const transformed3DPathString = transformed3DPath.transform(transform).toString();
SVGPathCommander comes with a full range of additional static methods, here's how to normalize a path:
const path = 'M0 0 H50';
const normalizedPath = SVGPathCommander.normalizePath(path);
// result => [['M', 0, 0], ['L', 50, 0]]
Reverse a path:
const path = SVGPathCommander.parsePathString('M0 0 H50');
const reversedPath = SVGPathCommander.reversePath(path);
// result => [['M', 50, 0], ['H', 0]]
Export to string:
const myPathString = SVGPathCommander.pathToString([['M', 0, 0], ['L', 50, 0]]);
// result => 'M0 0 L50 0'
Check a path string validity:
SVGPathCommander.isValidPath(path);
// result => boolean
Check if path is a certain kind of PathArray:
SVGPathCommander.isAbsoluteArray([['M', 0, 0], ['L', 50, 0]]);
// result => true
Use treeshake and create a custom function to apply a 3D transformation using static methods:
import { parsePathString, getPathBBox, transformPath, pathToString } from 'svg-path-commander';
function myTransformFn(pathInput: string | PathArray, transformObject: TransformObject) {
const path = parsePathString(pathInput);
const { cx, cy, cz } = getPathBBox(path);
return pathToString(
transformPath(path, {
...transformObject, origin: [cx, cy, cz]
})
)
}
In extreme cases where performance is paramount, you can consider the parent SVG viewBox attribute to extract a bounding box required for a consistent transform origin.
// const svgViewBox = document.getElementById('my-svg').getAttribute('viewBox');
const viewBox = '0 0 24 24';
const [x, y, width, height] = viewBox.split(/\s/).map(Number);
const origin = [
x + width / 2, // CX
y + height / 2, // CY
Math.max(width, height) + Math.min(width, height) / 2, // CZ
];
// use this origin for your shape transformation
const myNewString = new SVGPathCommander('M0 0 H50')
.transform({ rotate: [35, 0, 0], origin })
.toString();
Convert a shape to <path> and transfer all non-specific attributes
const myCircle = document.getElementById('myCircle');
SVGPathCommander.shapeToPath(myCircle, true);
Alternatively you can create <path> from specific attributes:
const myRectAttr = {
type: 'rect',
x: 25,
y: 25,
width: 50,
height: 50,
rx: 5
};
const myRectPath = SVGPathCommander.shapeToPath(myRectAttr);
document.getElementById('mySVG').append(myRectPath);
Server-side using jsdom:
const { document } = new JSDOM(
`<html>
<head></head>
<body>
<svg id="mySVG" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<rect id="myRect" x="0" width="100" height="100" rx="15" />
</svg>
</body>
</html>`,
{
pretendToBeVisual: true,
}
).window;
const myRect = document.getElementById('myRect');
SVGPathCommander.shapeToPath(myRect, true, document);
Get the path length:
const myPathLength = SVGPathCommander.getTotalLength('M0 0L50 0L25 50z');
// result => 161.80339887498948
Get a point along the path:
const myPoint = SVGPathCommander.getPointAtLength('M0 0L50 0L25 50z', 85);
// result => {x: 34.34752415750147, y: 31.304951684997057}
Get the path bounding box:
const myPathBBox = SVGPathCommander.getPathBBox('M0 0L50 0L25 50z');
// result => {width: 50, height: 50, x: 0, y: 0, x2: 50, y2: 50, cx: 25, cy: 25, cz: 75}
Find intersection points between two paths:
const count = SVGPathCommander.pathsIntersection(
'M0 50C0 0,100 0,100 50',
'M50 0C100 0,100 100,50 100',
true
);
// result => 1
const points = SVGPathCommander.pathsIntersection(
'M0 50C0 0,100 0,100 50',
'M50 0C100 0,100 100,50 100',
false
);
// result => [{ x: 50, y: 25, t1: 0.5, t2: 0.5 }]
Equalize two paths for morphing (single subpath):
const [eq1, eq2] = SVGPathCommander.equalizeSegments(
'M0 0L100 0L50 100Z',
'M0 0L100 0L100 100L0 100Z'
);
// eq1.length === eq2.length — ready for morphing
Equalize two paths for morphing (single or multi subpath):
const [eq1, eq2] = SVGPathCommander.equalizePaths(
'M0 0L100 0L50 100Z',
'M0 0L100 0L100 100L0 100Z',
{ close: true }
);
// Both paths now have the same number of segments
For developer guidelines, and a complete list of static methods, head over to the wiki/ folder in this repository.
optimize() instance method will not merge path segments (for instance two or more cubic-bezier segments into one or more arc segments); however, the script will try to provide shorthand notations where possible, pick the shortest string for each segment, and generally try to deliver the best possible outcome;pathToString, optimizePath and especially roundPath will always round values to the default of 4 decimals; EG: 0.56676 => 0.567, 0.50 => 0.5; you can change the default option with SVGPathCommander.options.round = 2 or remove the value rounding all together with SVGPathCommander.options.round = false; you can also control this feature via instance options;getSVGMatrix utility we developed will always compute the matrix by applying the transform functions in the following order: translate, rotate, skew and scale, which is the default composition/recomposition order specified in the W3C draft;A (arc) path commands to C (cubic bezier) due to the lack of resources;A segments to C segments is the shipped behavior, a validated arc-preserving alternative exists: an affine-transformed elliptical arc is still an exact elliptical arc, so A segments can be transformed directly via the singular value decomposition of the transformed ellipse shape matrix (radii and rotation from the eigenvalues/eigenvectors of the Gram matrix, sweep flag flipped for negative-determinant matrices). This works for any 2D matrix, and even for 3D matrices whenever the projection is orthographic (default origin: [0,0,0] — the z = 0 plane is untouched by the matrix's z rows/columns, so only the 2x2 submatrix matters); the conversion to C is only mathematically required for perspective projections (non-zero origin z value or a matrix with perspective), where the arc image is a general conic. See experiments/transformArc-tested.ts for the implementation and rationale;R (catmulRomBezier), O, U (ellipse and shorthand ellipse) are not present in the current draft and are not supported;SVGPathElement.getTotalLength() or SVGPathElement.getPointAtLength(), the output of our static methods is within a [0.002 - 0.05] margin delta, but from our experience it's proven to be a more consistent outcome.SVGPathCommander is released under MIT Licence.
Overall
Maintained
Code review
Latest release
Releases this year
4,648 followers · starred Oct 2022
258 followers · starred Mar 2023
67 followers · starred Jun 2024
36 followers · starred May 2023