From bfc74b07532e9f4d9de8b8f51971d5abebf9dd6b Mon Sep 17 00:00:00 2001 From: Virali Purbey Date: Mon, 30 Mar 2026 14:53:49 +0530 Subject: [PATCH 1/3] Add SVG Path Data API explainer Proposes adding getPathData(), setPathData(), and getPathSegmentAtLength() to SVGPathElement, implementing the SVG Paths spec DOM interfaces. Already shipped in Firefox 137+. --- README.md | 1 + SVG/SVGPathDataAPI/explainer.md | 238 ++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 SVG/SVGPathDataAPI/explainer.md diff --git a/README.md b/README.md index cfa4dc1a0..795e3edce 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,7 @@ we move them into the [Alumni section](#alumni-) below. | [Expose Content-Encoding in Resource Timing](ResourceTimingContentEncoding/Content_Encoding.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/Resource%20Timing%20Content%20Encoding?label=issues) | [New Issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?template=resource-timing-content-encoding.md) | Web Perf | | [Expose resource dependency in Resource Timing](ResourceTimingInitiatorInfo/explainer.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/Resource%20Timing%20Initiator%20Info?label=issues) | [New Issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?template=resource-timing-initiator-info.md) | Web Perf | | [Allow SVG `use` to reference entire files](SVG/allow-use-to-reference-entire-files.md)| ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/SVG%20Use%20reference%20entire%20file?label=issues)| [New issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?template=allow-use-to-reference-entire-files.md)| SVG | +| [SVG Path Data API](SVG/SVGPathDataAPI/explainer.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/SVGPathDataAPI?label=issues) | [New issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?labels=SVGPathDataAPI&title=%5BSVGPathDataAPI%5D+) | SVG | | [Gamepad Event-Driven Input API](GamepadEventDrivenInputAPI/explainer.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/Gamepad%20Event-Driven%20Input%20API?label=issues) | [New issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?template=gamepad-event-driven-input-api.md) | Gamepad | | [JS Self-Profiling API: Conditional Marker Exposure](ConditionalMarkersExposure/explainer.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/Conditional%20Marker%20Exposure?label=issues) | [New Issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?assignees=heathcliff-msft&labels=Conditional%20Marker%20Exposure&title=%5BConditional+Marker+Exposure%5D+%3CTITLE+HERE%3E) | Performance | | [GetSelectionBoundingClientRect()](GetSelectionBoundingClientRect/explainer.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/GetSelectionBoundingClientRect?label=issues) | [New Issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?template=getSelectionBoundingClientRect.md) | DOM | diff --git a/SVG/SVGPathDataAPI/explainer.md b/SVG/SVGPathDataAPI/explainer.md new file mode 100644 index 000000000..83023c3ac --- /dev/null +++ b/SVG/SVGPathDataAPI/explainer.md @@ -0,0 +1,238 @@ +# SVG Path Data API + +**Written:** 2026-03-30, **Updated:** 2026-03-30 + +## Authors + +- Virali Purbey (viralipurbey@microsoft.com) + +## Status of this Document + +This document is an **in-progress** explainer. + +## Participate + +- [Issue #1289](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/1289) (explainer feedback) +- [Chromium bug 40441025](https://issues.chromium.org/issues/40441025) (45 upvotes, filed Oct 2015) +- [SVG Paths §7 - DOM Interfaces](https://svgwg.org/specs/paths/#DOMInterfaces) (specification) +- [Firefox bug 1934525](https://bugzilla.mozilla.org/show_bug.cgi?id=1934525) (implementation) · [bug 1954044](https://bugzilla.mozilla.org/show_bug.cgi?id=1954044) (POJO fix) +- [w3c/editing#483](https://github.com/w3c/editing/issues/483) (POJO compat) · [w3c/svgwg#974](https://github.com/w3c/svgwg/issues/974) (constructability) + +## Table of Contents + +- [Introduction](#introduction) +- [User-Facing Problem](#user-facing-problem) +- [Goals](#goals) +- [Non-Goals](#non-goals) +- [User Research](#user-research) +- [Proposed Approach](#proposed-approach) +- [Key Design Decisions](#key-design-decisions) +- [Alternatives Considered](#alternatives-considered) +- [Accessibility, Internationalization, Privacy, and Security Considerations](#accessibility-internationalization-privacy-and-security-considerations) +- [Stakeholder Feedback / Opposition](#stakeholder-feedback--opposition) +- [References & Acknowledgements](#references--acknowledgements) +- [Appendix: WebIDL](#appendix-webidl) + +--- + +## 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](https://svgwg.org/specs/paths/#DOMInterfaces) 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 `` 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** (2015) 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 2015 | ❌ 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](https://github.com/jarek-foksa/path-data-polyfill) (129+ stars, de facto standard), manual `d` string parsing, and [pathseg polyfill](https://github.com/progers/pathseg). 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](https://issues.chromium.org/issues/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 + +```js +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}); +``` + +#### `setPathData(pathData)` - write segments (accepts POJOs) + +```js +path.setPathData([ + {type: "M", values: [0, 0]}, + {type: "L", values: [100, 0]}, + {type: "L", values: [50, 100]}, + {type: "Z", values: []} +]); +``` + +#### `getPathSegmentAtLength(distance)` - segment at distance + +```js +path.getPathSegmentAtLength(50); +// → {type: "C", values: [40, 10, 65, 10, 95, 80]} +``` + +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](https://svgwg.org/specs/paths/#DOMInterfaces) 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. + +### Before and after + +```js +// 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 + +```js +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](#appendix-webidl). + +--- + +## 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](https://bugzilla.mozilla.org/show_bug.cgi?id=1954044) 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](https://bugs.webkit.org/show_bug.cgi?id=260894)) | +| **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, no IPC, no untrusted data. `setPathData()` goes through the existing hardened `setAttribute("d")` code path. Gated behind a feature flag. + +--- + +## Stakeholder Feedback / Opposition + +| Stakeholder | Signal | Evidence | +|---|---|---| +| **Firefox** | ✅ Positive | Shipped [Firefox 137](https://bugzilla.mozilla.org/show_bug.cgi?id=1934525) (Jan 2025); [POJO fix](https://bugzilla.mozilla.org/show_bug.cgi?id=1954044) in 138 | +| **Safari/WebKit** | No signal | Still ships old API; [removal bug](https://bugs.webkit.org/show_bug.cgi?id=260894) open | +| **Web developers** | ✅ Strongly positive | 45 upvotes, 31 comments, enterprise breakage reports, 129+ polyfill stars | +| **SVG WG** | ✅ Positive | API in [consensus spec](https://svgwg.org/specs/paths/#DOMInterfaces) | +| **fs@opera.com** | ✅ Positive | Filed original bug; confirmed dictionary approach | + +--- + +## References & Acknowledgements + +**Specs:** [SVG Paths](https://svgwg.org/specs/paths/) · [SVG Paths §7 DOM Interfaces](https://svgwg.org/specs/paths/#DOMInterfaces) · [SVG 2](https://svgwg.org/svg2-draft/) + +**Bugs:** [Chromium 40441025](https://issues.chromium.org/issues/40441025) · [Firefox 1934525](https://bugzilla.mozilla.org/show_bug.cgi?id=1934525) · [Firefox 1954044](https://bugzilla.mozilla.org/show_bug.cgi?id=1954044) · [WebKit 260894](https://bugs.webkit.org/show_bug.cgi?id=260894) + +**Discussions:** [w3c/editing#483](https://github.com/w3c/editing/issues/483) · [w3c/svgwg#974](https://github.com/w3c/svgwg/issues/974) + +**Prior art:** [path-data-polyfill](https://github.com/jarek-foksa/path-data-polyfill) (129+ stars) · [pathseg polyfill](https://github.com/progers/pathseg) · [Interop hotlist](https://issues.chromium.org/hotlists/5575920) + +**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 + +```webidl +dictionary SVGPathSegment { + required DOMString type; + required sequence values; +}; + +dictionary SVGPathDataSettings { + boolean normalize = false; +}; + +partial interface SVGPathElement { + sequence getPathData(optional SVGPathDataSettings settings = {}); + undefined setPathData(sequence pathData); + SVGPathSegment? getPathSegmentAtLength(unrestricted float distance); +}; +``` + +**Spec text updates (PR pending):** `dictionary` instead of `[NoInterfaceObject] interface` (accepts POJOs natively); `unrestricted float` instead of `float` (matches SVG error model); `required` keywords added (prevents `setPathData([{}])`). From 85d6098e59a73a51694bcba6777d414f0c62d08e Mon Sep 17 00:00:00 2001 From: Virali Purbey Date: Mon, 30 Mar 2026 15:28:05 +0530 Subject: [PATCH 2/3] Address review feedback on SVG Path Data API explainer - Fix security claim: setPathData() operates on structured data, no string parsing needed (does not go through setAttribute) - Add arc-to-cubic normalization precision note - Document empty path behavior: getPathData() returns [], setPathData([]) clears the path - Document getPathSegmentAtLength edge cases: null for empty/NaN, clamp negative to 0 - Split before/after example into separate code blocks to avoid variable redeclaration - Fix Chrome 48 date from 2015 to Jan 2016 (stable release date) - Fix spec PR status wording: 'to be filed' instead of 'pending' --- SVG/SVGPathDataAPI/explainer.md | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/SVG/SVGPathDataAPI/explainer.md b/SVG/SVGPathDataAPI/explainer.md index 83023c3ac..be9a34dae 100644 --- a/SVG/SVGPathDataAPI/explainer.md +++ b/SVG/SVGPathDataAPI/explainer.md @@ -47,11 +47,11 @@ The API is specified in the [SVG Paths](https://svgwg.org/specs/paths/#DOMInterf SVG `` 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** (2015) 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. +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 2015 | ❌ Not yet | +| Chrome | ❌ Removed Jan 2016 | ❌ Not yet | | Firefox | ❌ Removed 2018 | ✅ Shipped Jan 2025 | | Safari | ✅ Still supported | ❌ Not yet | @@ -102,6 +102,9 @@ const segments = path.getPathData(); // 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) @@ -113,6 +116,9 @@ path.setPathData([ {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 @@ -120,12 +126,23 @@ path.setPathData([ ```js 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](https://svgwg.org/specs/paths/#DOMInterfaces) 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 ```js @@ -134,7 +151,9 @@ const d = path.getAttribute('d'); const segments = myCustomParser(d); // or load ~4KB polyfill segments[1].values[0] = 50; path.setAttribute('d', myCustomSerializer(segments)); +``` +```js // AFTER: native, zero dependencies const segments = path.getPathData(); segments[1].values[0] = 50; @@ -186,7 +205,7 @@ The formal WebIDL is in the [Appendix](#appendix-webidl). - **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, no IPC, no untrusted data. `setPathData()` goes through the existing hardened `setAttribute("d")` code path. Gated behind a feature flag. +- **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. --- @@ -235,4 +254,4 @@ partial interface SVGPathElement { }; ``` -**Spec text updates (PR pending):** `dictionary` instead of `[NoInterfaceObject] interface` (accepts POJOs natively); `unrestricted float` instead of `float` (matches SVG error model); `required` keywords added (prevents `setPathData([{}])`). +**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([{}])`). From fc5acfdec8fbb81b896fc17d9e4be52d0308cec3 Mon Sep 17 00:00:00 2001 From: Virali Purbey Date: Wed, 1 Apr 2026 15:21:41 +0530 Subject: [PATCH 3/3] addressed comments --- SVG/SVGPathDataAPI/explainer.md | 106 ++++++++++++++++---------------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/SVG/SVGPathDataAPI/explainer.md b/SVG/SVGPathDataAPI/explainer.md index be9a34dae..bae570914 100644 --- a/SVG/SVGPathDataAPI/explainer.md +++ b/SVG/SVGPathDataAPI/explainer.md @@ -1,6 +1,6 @@ # SVG Path Data API -**Written:** 2026-03-30, **Updated:** 2026-03-30 +**Written:** 2026-03-30, **Updated:** 2026-04-06 ## Authors @@ -8,7 +8,7 @@ ## Status of this Document -This document is an **in-progress** explainer. +This document is a **short explainer** for an implementation of an existing consensus standard ([SVG Paths §7 DOM Interfaces](https://svgwg.org/specs/paths/#DOMInterfaces)). No new web platform concepts are introduced. This explainer captures developer benefit, key implementation decisions, and Chromium-specific shipping details; it is intentionally concise since the API was designed by the SVG WG, not the authors of this document. ## Participate @@ -24,22 +24,21 @@ This document is an **in-progress** explainer. - [User-Facing Problem](#user-facing-problem) - [Goals](#goals) - [Non-Goals](#non-goals) -- [User Research](#user-research) - [Proposed Approach](#proposed-approach) - [Key Design Decisions](#key-design-decisions) - [Alternatives Considered](#alternatives-considered) - [Accessibility, Internationalization, Privacy, and Security Considerations](#accessibility-internationalization-privacy-and-security-considerations) - [Stakeholder Feedback / Opposition](#stakeholder-feedback--opposition) - [References & Acknowledgements](#references--acknowledgements) +- [Testing](#testing) +- [Implementation Notes](#implementation-notes) - [Appendix: WebIDL](#appendix-webidl) --- ## 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](https://svgwg.org/specs/paths/#DOMInterfaces) 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. +Chrome has had **no native way** to read or write individual SVG path segments since Chrome 48 (early 2016). This explainer proposes implementing `getPathData()`, `setPathData()`, and `getPathSegmentAtLength()` on `SVGPathElement`, as specified in the [SVG Paths](https://svgwg.org/specs/paths/#DOMInterfaces) W3C Editor's Draft and already shipped in **Firefox 137+** (Apr 2025). --- @@ -51,14 +50,16 @@ Chromium removed the old `SVGPathSegList` API in **Chrome 48** (Jan 2016) becaus | 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 | +| Chrome | ❌ Removed Jan 2016 | ❌ Not implemented | +| Firefox | ❌ Removed 2018 | ✅ Shipped Apr 2025 | +| Safari | ✅ Still supported | ❌ Not implemented | **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](https://github.com/jarek-foksa/path-data-polyfill) (129+ stars, de facto standard), manual `d` string parsing, and [pathseg polyfill](https://github.com/progers/pathseg). All are slower than native and add unnecessary JS weight. +**Developer demand:** [crbug.com/40441025](https://issues.chromium.org/issues/40441025) has **45 upvotes** and **31 comments** from enterprise developers and library authors over 10 years. Five Sheriffbot closure attempts (2017-2021) were each reopened. + --- ## Goals @@ -78,21 +79,9 @@ Chromium removed the old `SVGPathSegList` API in **Chrome 48** (Jan 2016) becaus --- -## User Research - -No formal study was conducted, but 10 years of organic feedback on [crbug.com/40441025](https://issues.chromium.org/issues/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: +Three methods are added to `SVGPathElement`, using simple `{type, values}` plain objects (no dependencies on non-stable features): #### `getPathData(settings)` - read segments @@ -117,8 +106,11 @@ path.setPathData([ {type: "Z", values: []} ]); -// Passing an empty array clears the path (equivalent to setAttribute('d', '')) +// Passing an empty array clears the path: sets d="" (equivalent to setAttribute('d', ''), +// NOT removeAttribute('d') - the attribute remains present but empty). Matches Firefox. path.setPathData([]); +// getPathData() on an empty/cleared path returns [] +emptyPath.getPathData(); // → [] ``` #### `getPathSegmentAtLength(distance)` - segment at distance @@ -135,13 +127,17 @@ path.getPathSegmentAtLength(-10); // → {type: "M", values: [10, 80]} // NaN returns null path.getPathSegmentAtLength(NaN); // → null + +// Distances exceeding getTotalLength() clamp to the path's total length (returns last segment), +// matching getPointAtLength() clamping behavior per the SVG spec. +path.getPathSegmentAtLength(99999); // → last segment (e.g. {type: "Z", values: []}) ``` 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](https://svgwg.org/specs/paths/#DOMInterfaces) 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. +> **Note:** Arc-to-cubic conversion (A → C) is an approximation (inherently lossy); quadratic-to-cubic (Q → C) is exact. Precision details will be in the design doc. ### Before and after @@ -160,29 +156,17 @@ segments[1].values[0] = 50; path.setPathData(segments); ``` -### Example: path morphing - -```js -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](#appendix-webidl). --- ## 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](https://bugzilla.mozilla.org/show_bug.cgi?id=1954044) to accept plain objects in Firefox 138. Using a dictionary from the start avoids this. +1. **Plain objects, not class instances.** We use a WebIDL `dictionary`, so `setPathData()` accepts plain `{type, values}` POJOs natively. The SVG WG confirmed this approach in [w3c/svgwg#1082](https://github.com/w3c/svgwg/issues/1082). Firefox initially required interface instances (Firefox 137), which caused polyfill compatibility issues, and later [updated](https://bugzilla.mozilla.org/show_bug.cgi?id=1954044) 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. +3. **Two-level validation in `setPathData()`.** WebIDL enforces structural validity: both `type` and `values` must be present, or a `TypeError` is thrown (e.g., `setPathData([{}])` or `setPathData([{type: "L"}])` throws). Semantic validation is lenient: unrecognized type strings or incorrect `values` array lengths cause the segment to be silently 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. @@ -190,13 +174,9 @@ The formal WebIDL is in the [Appendix](#appendix-webidl). ## 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](https://bugs.webkit.org/show_bug.cgi?id=260894)) | -| **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 | +The API shape was designed by the SVG WG, not the authors of this document. The main alternative - re-implementing the old `SVGPathSegList` API - was rejected by the WG because of its complexity (20+ factory methods, live mutation semantics). No modern engine is adding new `SVGPathSegList` support ([WebKit removal bug](https://bugs.webkit.org/show_bug.cgi?id=260894)). + +Our implementation-specific choices (dictionary vs interface, `unrestricted float`, lenient validation) are documented in [Key Design Decisions](#key-design-decisions) above. --- @@ -205,7 +185,7 @@ The formal WebIDL is in the [Appendix](#appendix-webidl). - **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. +- **Security:** No new concerns. Operates entirely within the renderer on already-structured data. `setPathData()` operates on structured `{type, values}` dictionaries - no string parsing is needed (segments are pre-typed), reducing attack surface compared to `setAttribute('d')`. No additional IPC beyond existing DOM access. Gated behind a Blink `RuntimeEnabledFeature` (`SVGPathDataAPI`). --- @@ -213,11 +193,10 @@ The formal WebIDL is in the [Appendix](#appendix-webidl). | Stakeholder | Signal | Evidence | |---|---|---| -| **Firefox** | ✅ Positive | Shipped [Firefox 137](https://bugzilla.mozilla.org/show_bug.cgi?id=1934525) (Jan 2025); [POJO fix](https://bugzilla.mozilla.org/show_bug.cgi?id=1954044) in 138 | -| **Safari/WebKit** | No signal | Still ships old API; [removal bug](https://bugs.webkit.org/show_bug.cgi?id=260894) open | +| **Firefox** | ✅ Positive | Shipped [Firefox 137](https://bugzilla.mozilla.org/show_bug.cgi?id=1934525) (Apr 2025); [POJO fix](https://bugzilla.mozilla.org/show_bug.cgi?id=1954044) in 138 | +| **Safari/WebKit** | No signal | Still ships old API; [removal bug](https://bugs.webkit.org/show_bug.cgi?id=260894) open (TODO: file WebKit standards position request) | | **Web developers** | ✅ Strongly positive | 45 upvotes, 31 comments, enterprise breakage reports, 129+ polyfill stars | -| **SVG WG** | ✅ Positive | API in [consensus spec](https://svgwg.org/specs/paths/#DOMInterfaces) | -| **fs@opera.com** | ✅ Positive | Filed original bug; confirmed dictionary approach | +| **SVG WG** | ✅ Positive | API in [consensus spec](https://svgwg.org/specs/paths/#DOMInterfaces); dictionary approach confirmed in [w3c/svgwg#1082](https://github.com/w3c/svgwg/issues/1082) | --- @@ -227,14 +206,35 @@ The formal WebIDL is in the [Appendix](#appendix-webidl). **Bugs:** [Chromium 40441025](https://issues.chromium.org/issues/40441025) · [Firefox 1934525](https://bugzilla.mozilla.org/show_bug.cgi?id=1934525) · [Firefox 1954044](https://bugzilla.mozilla.org/show_bug.cgi?id=1954044) · [WebKit 260894](https://bugs.webkit.org/show_bug.cgi?id=260894) -**Discussions:** [w3c/editing#483](https://github.com/w3c/editing/issues/483) · [w3c/svgwg#974](https://github.com/w3c/svgwg/issues/974) +**Discussions:** [w3c/editing#483](https://github.com/w3c/editing/issues/483) · [w3c/svgwg#974](https://github.com/w3c/svgwg/issues/974) · [w3c/svgwg#1082](https://github.com/w3c/svgwg/issues/1082) (dictionary resolution) -**Prior art:** [path-data-polyfill](https://github.com/jarek-foksa/path-data-polyfill) (129+ stars) · [pathseg polyfill](https://github.com/progers/pathseg) · [Interop hotlist](https://issues.chromium.org/hotlists/5575920) +**Prior art:** [path-data-polyfill](https://github.com/jarek-foksa/path-data-polyfill) (129+ stars) · [pathseg polyfill](https://github.com/progers/pathseg) · [Interop hotlist](https://issues.chromium.org/hotlists/5575920) (Chromium cross-browser interop tracking; includes [crbug.com/40441025](https://issues.chromium.org/issues/40441025)) **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). --- +## Testing + +**Existing WPTs:** Firefox landed web-platform-tests alongside their implementation in [svg/path/interfaces/](https://wpt.fyi/results/svg/path/interfaces?label=experimental&label=master&aligned), including `SVGPathSegment.svg` which covers `getPathData()`, `setPathData()`, `getPathSegmentAtLength()`, normalization, and basic command coverage. + +**Planned additional tests:** +- Edge cases: empty paths, NaN/Infinity values, distance > totalLength clamping, negative distance clamping +- Normalization accuracy: arc-to-cubic precision, quadratic-to-cubic exactness +- POJO acceptance: plain `{type, values}` objects work without constructors +- Two-level validation: TypeError for missing required fields vs silent skip for semantic errors +- Blink layout tests for rendering integration + +--- + +## Implementation Notes + +**Feature flag:** This API will be gated behind a Blink `RuntimeEnabledFeature` named `SVGPathDataAPI`. It will not have a separate `chrome://flags` entry - it follows the standard Blink shipping process (flag → origin trial → ship). + +**UseCounters:** The implementation will include UseCounters for each method (`getPathData`, `setPathData`, `getPathSegmentAtLength`) to track adoption and inform the ship decision. No existing UseCounter data is available since the API does not yet exist in Blink. + +--- + ## Appendix: WebIDL ```webidl @@ -254,4 +254,4 @@ partial interface SVGPathElement { }; ``` -**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([{}])`). +**Spec text updates (spec PR to be filed):** `dictionary` instead of `[NoInterfaceObject] interface` (accepts POJOs natively; WG resolution: [w3c/svgwg#1082](https://github.com/w3c/svgwg/issues/1082)); `unrestricted float` instead of `float` (matches SVG error model); `required` keywords added (prevents `setPathData([{}])`).