Skip to content

Latest commit

 

History

History
257 lines (174 loc) · 12.7 KB

File metadata and controls

257 lines (174 loc) · 12.7 KB

SVG Path Data API

Written: 2026-03-30, Updated: 2026-03-30

Authors

Status of this Document

This document is an in-progress explainer.

Participate

Table of Contents


Introduction

Chrome has had no native way to read or write individual SVG path segments since 2015. This explainer proposes adding getPathData(), setPathData(), and getPathSegmentAtLength() to SVGPathElement, giving developers structured access to path segments as simple {type, values} objects.

The API is specified in the SVG Paths W3C Editor's Draft and has shipped in Firefox 137+ (Jan 2025). This implements an existing consensus standard - no new web platform concepts are introduced.


User-Facing Problem

SVG <path> elements define their shape through a d attribute string. Today in Chrome, the only way to inspect or modify individual path segments is to parse this raw string manually or include a polyfill. This can result in slower interactions, larger page loads, and degraded UX - especially on low-end devices.

Chromium removed the old SVGPathSegList API in Chrome 48 (Jan 2016) because it was overly complex and poorly specified. The SVG WG specified a cleaner replacement, but it has not yet been implemented in Chrome - a gap that has persisted for over 10 years.

Engine Old API (SVGPathSegList) New API (getPathData/setPathData)
Chrome ❌ Removed Jan 2016 ❌ Not yet
Firefox ❌ Removed 2018 ✅ Shipped Jan 2025
Safari ✅ Still supported ❌ Not yet

Who is affected: End users of SVG-heavy web apps (slower load times due to polyfills); data visualization developers (D3.js path morphing); SVG editor developers (Boxy SVG, SVG-Edit); animation developers (path interpolation).

Current workarounds: path-data-polyfill (129+ stars, de facto standard), manual d string parsing, and pathseg polyfill. All are slower than native and add unnecessary JS weight.


Goals

  1. Restore segment-level path access natively in Chrome.
  2. Interop with Firefox - match Firefox 137+'s shipped behavior.
  3. Polyfill compatibility - code using path-data-polyfill should work unchanged with the native API.
  4. Normalization support - getPathData({normalize: true}) returns only absolute M, L, C, Z.

Non-Goals

  • Restoring SVGPathSegList - the old API is not being brought back.
  • Path editing UI - programmatic API only.
  • Animated path data - returns base value only, not current animated value.
  • New path commands - no Catmull-Rom (R) or Bearing (B); no browser supports them.
  • SVGPathSegment constructor - the SVG WG resolved that a constructor is not needed at this time; our dictionary approach aligns with this.

User Research

No formal study was conducted, but 10 years of organic feedback on crbug.com/40441025 provides helpful signal: 45 upvotes, 31 comments (enterprise developers, library authors), 129+ GitHub stars on the polyfill, and 5 Sheriffbot closure attempts survived (2017-2021, each reopened by fs@opera.com).

"In our B2B solution for glasses design we have round about 1000 users which can not work since the last Google Chrome update." - Jan 2016

"We'll soon celebrate the 10th anniversary of this issue. It's... a long time." - Jun 2025


Proposed Approach

Dependencies on non-stable features: None.

Three methods are added to SVGPathElement, using simple {type, values} plain objects:

getPathData(settings) - read segments

const segments = path.getPathData();
// → [{type: "M", values: [10, 80]}, {type: "C", values: [40, 10, 65, 10, 95, 80]}, ...]

// Normalize: all segments converted to absolute M, L, C, Z
const normalized = path.getPathData({normalize: true});

// Empty or missing d attribute returns an empty array
emptyPath.getPathData();  // → []

setPathData(pathData) - write segments (accepts POJOs)

path.setPathData([
  {type: "M", values: [0, 0]},
  {type: "L", values: [100, 0]},
  {type: "L", values: [50, 100]},
  {type: "Z", values: []}
]);

// Passing an empty array clears the path (equivalent to setAttribute('d', ''))
path.setPathData([]);

getPathSegmentAtLength(distance) - segment at distance

path.getPathSegmentAtLength(50);
// → {type: "C", values: [40, 10, 65, 10, 95, 80]}

// Returns null if the path is empty or has no length
emptyPath.getPathSegmentAtLength(10);  // → null

// Negative distances clamp to 0 (returns the first segment), matching getPointAtLength() behavior
path.getPathSegmentAtLength(-10);  // → {type: "M", values: [10, 80]}

// NaN returns null
path.getPathSegmentAtLength(NaN);  // → null

All 20 SVG path commands (M, m, L, l, H, h, V, v, C, c, S, s, Q, q, T, t, A, a, Z, z) are supported. See the spec for the full type/values mapping.

Normalization ({normalize: true}) converts all segments to absolute M, L, C, Z only - relative to absolute, H/V to L, Q/T to C, S to C, A to C. Consumers need only handle 4 command types.

Note: Arc-to-cubic conversion (A → C) is an approximation using midpoint subdivision and is inherently lossy. The precision matches the existing getTotalLength()/getPointAtLength() code path in Blink. For most use cases the approximation error is sub-pixel.

Before and after

// BEFORE: parse d-string manually or include a polyfill
const d = path.getAttribute('d');
const segments = myCustomParser(d);  // or load ~4KB polyfill
segments[1].values[0] = 50;
path.setAttribute('d', myCustomSerializer(segments));
// AFTER: native, zero dependencies
const segments = path.getPathData();
segments[1].values[0] = 50;
path.setPathData(segments);

Example: path morphing

const segA = pathA.getPathData({normalize: true});
const segB = pathB.getPathData({normalize: true});
const interpolate = (t) => segA.map((s, i) => ({
  type: s.type,
  values: s.values.map((v, j) => v + (segB[i].values[j] - v) * t)
}));
pathTarget.setPathData(interpolate(0.5));

The formal WebIDL is in the Appendix.


Key Design Decisions

  1. Plain objects, not class instances. We use a WebIDL dictionary, so setPathData() accepts plain {type, values} POJOs natively. Firefox initially required interface instances (Firefox 137), which caused polyfill compatibility issues, and later updated to accept plain objects in Firefox 138. Using a dictionary from the start avoids this.

  2. unrestricted float for values. NaN/Infinity are accepted without throwing, matching SVG's graceful error model and Firefox's behavior.

  3. Invalid segments silently skipped. Unrecognized types or wrong value counts in setPathData() are skipped (not thrown), matching SVG's "render what you can" model, Firefox, and the polyfill.

  4. Returns base value, not animated value. getPathData() returns the d attribute's base value, consistent with getAttribute('d') and Firefox.


Alternatives Considered

Alternative Why rejected
Re-implement SVGPathSegList SVG WG removed it from SVG 2; live mutation is complex; 20+ factory methods; no modern engine adding new support (WebKit removal bug)
Use interface per spec text Would not accept plain objects from polyfill code; Firefox encountered this and updated to accept POJOs; spec author confirmed dictionary was the intent
Use float (not unrestricted) SVG renders degenerate paths as empty rather than erroring; would affect polyfill-based code; Firefox uses unrestricted
Throw on invalid segments Firefox and polyfill skip silently; SVG model is "render what you can"
Return animated value No use case identified; adds complexity; inconsistent with getAttribute('d'); Firefox returns base

Accessibility, Internationalization, Privacy, and Security Considerations

  • Accessibility: No impact. Programmatic API only - no new visual content, interaction patterns, or ARIA roles. Indirectly benefits a11y by making it easier to build well-structured SVG.
  • Internationalization: No impact. Path data uses single-character Latin commands and numbers only.
  • Privacy: No new concerns. Returns the same data available via getAttribute('d') - purely a convenience API over existing capabilities. No fingerprinting surface, no network requests.
  • Security: No new concerns. Operates entirely within the renderer on already-structured data. No string parsing is needed (segments are pre-typed), reducing attack surface compared to setAttribute('d'). No IPC. Gated behind a feature flag.

Stakeholder Feedback / Opposition

Stakeholder Signal Evidence
Firefox ✅ Positive Shipped Firefox 137 (Jan 2025); POJO fix in 138
Safari/WebKit No signal Still ships old API; removal bug open
Web developers ✅ Strongly positive 45 upvotes, 31 comments, enterprise breakage reports, 129+ polyfill stars
SVG WG ✅ Positive API in consensus spec
fs@opera.com ✅ Positive Filed original bug; confirmed dictionary approach

References & Acknowledgements

Specs: SVG Paths · SVG Paths §7 DOM Interfaces · SVG 2

Bugs: Chromium 40441025 · Firefox 1934525 · Firefox 1954044 · WebKit 260894

Discussions: w3c/editing#483 · w3c/svgwg#974

Prior art: path-data-polyfill (129+ stars) · pathseg polyfill · Interop hotlist

Acknowledgements: Fredrik Söderquist (fs@opera.com, original API sketch author, SVG OWNERS), Philip Rogers (pdr@chromium.org, drove SVGPathSegList removal, pathseg polyfill), Robert Longson (Mozilla SVG lead, Firefox implementation), Jarek Foksa (path-data-polyfill author), Cameron McCormack (spec editor).


Appendix: WebIDL

dictionary SVGPathSegment {
  required DOMString type;
  required sequence<unrestricted float> values;
};

dictionary SVGPathDataSettings {
  boolean normalize = false;
};

partial interface SVGPathElement {
  sequence<SVGPathSegment> getPathData(optional SVGPathDataSettings settings = {});
  undefined setPathData(sequence<SVGPathSegment> pathData);
  SVGPathSegment? getPathSegmentAtLength(unrestricted float distance);
};

Spec text updates (spec PR to be filed): dictionary instead of [NoInterfaceObject] interface (accepts POJOs natively); unrestricted float instead of float (matches SVG error model); required keywords added (prevents setPathData([{}])).