|
| 1 | +# SVG Path Data API |
| 2 | + |
| 3 | +**Written:** 2026-03-30, **Updated:** 2026-03-30 |
| 4 | + |
| 5 | +## Authors |
| 6 | + |
| 7 | +- Virali Purbey (viralipurbey@microsoft.com) |
| 8 | + |
| 9 | +## Status of this Document |
| 10 | + |
| 11 | +This document is an **in-progress** explainer. |
| 12 | + |
| 13 | +## Participate |
| 14 | + |
| 15 | +- [Issue #1289](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/1289) (explainer feedback) |
| 16 | +- [Chromium bug 40441025](https://issues.chromium.org/issues/40441025) (45 upvotes, filed Oct 2015) |
| 17 | +- [SVG Paths §7 - DOM Interfaces](https://svgwg.org/specs/paths/#DOMInterfaces) (specification) |
| 18 | +- [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) |
| 19 | +- [w3c/editing#483](https://github.com/w3c/editing/issues/483) (POJO compat) · [w3c/svgwg#974](https://github.com/w3c/svgwg/issues/974) (constructability) |
| 20 | + |
| 21 | +## Table of Contents |
| 22 | + |
| 23 | +- [Introduction](#introduction) |
| 24 | +- [User-Facing Problem](#user-facing-problem) |
| 25 | +- [Goals](#goals) |
| 26 | +- [Non-Goals](#non-goals) |
| 27 | +- [User Research](#user-research) |
| 28 | +- [Proposed Approach](#proposed-approach) |
| 29 | +- [Key Design Decisions](#key-design-decisions) |
| 30 | +- [Alternatives Considered](#alternatives-considered) |
| 31 | +- [Accessibility, Internationalization, Privacy, and Security Considerations](#accessibility-internationalization-privacy-and-security-considerations) |
| 32 | +- [Stakeholder Feedback / Opposition](#stakeholder-feedback--opposition) |
| 33 | +- [References & Acknowledgements](#references--acknowledgements) |
| 34 | +- [Appendix: WebIDL](#appendix-webidl) |
| 35 | + |
| 36 | +--- |
| 37 | + |
| 38 | +## Introduction |
| 39 | + |
| 40 | +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. |
| 41 | + |
| 42 | +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. |
| 43 | + |
| 44 | +--- |
| 45 | + |
| 46 | +## User-Facing Problem |
| 47 | + |
| 48 | +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. |
| 49 | + |
| 50 | +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. |
| 51 | + |
| 52 | +| Engine | Old API (`SVGPathSegList`) | New API (`getPathData`/`setPathData`) | |
| 53 | +|--------|---------------------------|---------------------------------------| |
| 54 | +| Chrome | ❌ Removed 2015 | ❌ Not yet | |
| 55 | +| Firefox | ❌ Removed 2018 | ✅ Shipped Jan 2025 | |
| 56 | +| Safari | ✅ Still supported | ❌ Not yet | |
| 57 | + |
| 58 | +**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). |
| 59 | + |
| 60 | +**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. |
| 61 | + |
| 62 | +--- |
| 63 | + |
| 64 | +## Goals |
| 65 | + |
| 66 | +1. **Restore segment-level path access** natively in Chrome. |
| 67 | +2. **Interop with Firefox** - match Firefox 137+'s shipped behavior. |
| 68 | +3. **Polyfill compatibility** - code using path-data-polyfill should work unchanged with the native API. |
| 69 | +4. **Normalization support** - `getPathData({normalize: true})` returns only absolute M, L, C, Z. |
| 70 | + |
| 71 | +## Non-Goals |
| 72 | + |
| 73 | +- **Restoring `SVGPathSegList`** - the old API is not being brought back. |
| 74 | +- **Path editing UI** - programmatic API only. |
| 75 | +- **Animated path data** - returns base value only, not current animated value. |
| 76 | +- **New path commands** - no Catmull-Rom (`R`) or Bearing (`B`); no browser supports them. |
| 77 | +- **`SVGPathSegment` constructor** - the SVG WG resolved that a constructor is not needed at this time; our dictionary approach aligns with this. |
| 78 | + |
| 79 | +--- |
| 80 | + |
| 81 | +## User Research |
| 82 | + |
| 83 | +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). |
| 84 | + |
| 85 | +> *"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 |
| 86 | +
|
| 87 | +> *"We'll soon celebrate the 10th anniversary of this issue. It's... a long time."* - Jun 2025 |
| 88 | +
|
| 89 | +--- |
| 90 | + |
| 91 | +## Proposed Approach |
| 92 | + |
| 93 | +**Dependencies on non-stable features:** None. |
| 94 | + |
| 95 | +Three methods are added to `SVGPathElement`, using simple `{type, values}` plain objects: |
| 96 | + |
| 97 | +#### `getPathData(settings)` - read segments |
| 98 | + |
| 99 | +```js |
| 100 | +const segments = path.getPathData(); |
| 101 | +// → [{type: "M", values: [10, 80]}, {type: "C", values: [40, 10, 65, 10, 95, 80]}, ...] |
| 102 | + |
| 103 | +// Normalize: all segments converted to absolute M, L, C, Z |
| 104 | +const normalized = path.getPathData({normalize: true}); |
| 105 | +``` |
| 106 | + |
| 107 | +#### `setPathData(pathData)` - write segments (accepts POJOs) |
| 108 | + |
| 109 | +```js |
| 110 | +path.setPathData([ |
| 111 | + {type: "M", values: [0, 0]}, |
| 112 | + {type: "L", values: [100, 0]}, |
| 113 | + {type: "L", values: [50, 100]}, |
| 114 | + {type: "Z", values: []} |
| 115 | +]); |
| 116 | +``` |
| 117 | + |
| 118 | +#### `getPathSegmentAtLength(distance)` - segment at distance |
| 119 | + |
| 120 | +```js |
| 121 | +path.getPathSegmentAtLength(50); |
| 122 | +// → {type: "C", values: [40, 10, 65, 10, 95, 80]} |
| 123 | +``` |
| 124 | + |
| 125 | +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. |
| 126 | + |
| 127 | +**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. |
| 128 | + |
| 129 | +### Before and after |
| 130 | + |
| 131 | +```js |
| 132 | +// BEFORE: parse d-string manually or include a polyfill |
| 133 | +const d = path.getAttribute('d'); |
| 134 | +const segments = myCustomParser(d); // or load ~4KB polyfill |
| 135 | +segments[1].values[0] = 50; |
| 136 | +path.setAttribute('d', myCustomSerializer(segments)); |
| 137 | + |
| 138 | +// AFTER: native, zero dependencies |
| 139 | +const segments = path.getPathData(); |
| 140 | +segments[1].values[0] = 50; |
| 141 | +path.setPathData(segments); |
| 142 | +``` |
| 143 | + |
| 144 | +### Example: path morphing |
| 145 | + |
| 146 | +```js |
| 147 | +const segA = pathA.getPathData({normalize: true}); |
| 148 | +const segB = pathB.getPathData({normalize: true}); |
| 149 | +const interpolate = (t) => segA.map((s, i) => ({ |
| 150 | + type: s.type, |
| 151 | + values: s.values.map((v, j) => v + (segB[i].values[j] - v) * t) |
| 152 | +})); |
| 153 | +pathTarget.setPathData(interpolate(0.5)); |
| 154 | +``` |
| 155 | + |
| 156 | +The formal WebIDL is in the [Appendix](#appendix-webidl). |
| 157 | + |
| 158 | +--- |
| 159 | + |
| 160 | +## Key Design Decisions |
| 161 | + |
| 162 | +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. |
| 163 | + |
| 164 | +2. **`unrestricted float` for values.** NaN/Infinity are accepted without throwing, matching SVG's graceful error model and Firefox's behavior. |
| 165 | + |
| 166 | +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. |
| 167 | + |
| 168 | +4. **Returns base value, not animated value.** `getPathData()` returns the `d` attribute's base value, consistent with `getAttribute('d')` and Firefox. |
| 169 | + |
| 170 | +--- |
| 171 | + |
| 172 | +## Alternatives Considered |
| 173 | + |
| 174 | +| Alternative | Why rejected | |
| 175 | +|---|---| |
| 176 | +| **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)) | |
| 177 | +| **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 | |
| 178 | +| **Use `float` (not `unrestricted`)** | SVG renders degenerate paths as empty rather than erroring; would affect polyfill-based code; Firefox uses unrestricted | |
| 179 | +| **Throw on invalid segments** | Firefox and polyfill skip silently; SVG model is "render what you can" | |
| 180 | +| **Return animated value** | No use case identified; adds complexity; inconsistent with `getAttribute('d')`; Firefox returns base | |
| 181 | + |
| 182 | +--- |
| 183 | + |
| 184 | +## Accessibility, Internationalization, Privacy, and Security Considerations |
| 185 | + |
| 186 | +- **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. |
| 187 | +- **Internationalization:** No impact. Path data uses single-character Latin commands and numbers only. |
| 188 | +- **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. |
| 189 | +- **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. |
| 190 | + |
| 191 | +--- |
| 192 | + |
| 193 | +## Stakeholder Feedback / Opposition |
| 194 | + |
| 195 | +| Stakeholder | Signal | Evidence | |
| 196 | +|---|---|---| |
| 197 | +| **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 | |
| 198 | +| **Safari/WebKit** | No signal | Still ships old API; [removal bug](https://bugs.webkit.org/show_bug.cgi?id=260894) open | |
| 199 | +| **Web developers** | ✅ Strongly positive | 45 upvotes, 31 comments, enterprise breakage reports, 129+ polyfill stars | |
| 200 | +| **SVG WG** | ✅ Positive | API in [consensus spec](https://svgwg.org/specs/paths/#DOMInterfaces) | |
| 201 | +| **fs@opera.com** | ✅ Positive | Filed original bug; confirmed dictionary approach | |
| 202 | + |
| 203 | +--- |
| 204 | + |
| 205 | +## References & Acknowledgements |
| 206 | + |
| 207 | +**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/) |
| 208 | + |
| 209 | +**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) |
| 210 | + |
| 211 | +**Discussions:** [w3c/editing#483](https://github.com/w3c/editing/issues/483) · [w3c/svgwg#974](https://github.com/w3c/svgwg/issues/974) |
| 212 | + |
| 213 | +**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) |
| 214 | + |
| 215 | +**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). |
| 216 | + |
| 217 | +--- |
| 218 | + |
| 219 | +## Appendix: WebIDL |
| 220 | + |
| 221 | +```webidl |
| 222 | +dictionary SVGPathSegment { |
| 223 | + required DOMString type; |
| 224 | + required sequence<unrestricted float> values; |
| 225 | +}; |
| 226 | +
|
| 227 | +dictionary SVGPathDataSettings { |
| 228 | + boolean normalize = false; |
| 229 | +}; |
| 230 | +
|
| 231 | +partial interface SVGPathElement { |
| 232 | + sequence<SVGPathSegment> getPathData(optional SVGPathDataSettings settings = {}); |
| 233 | + undefined setPathData(sequence<SVGPathSegment> pathData); |
| 234 | + SVGPathSegment? getPathSegmentAtLength(unrestricted float distance); |
| 235 | +}; |
| 236 | +``` |
| 237 | + |
| 238 | +**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([{}])`). |
0 commit comments