SVG paths are written as a single d attribute — a mini-language of commands and coordinates that describes shapes. Each command is a letter; uppercase means absolute coordinates, lowercase means relative to the current position. This is a reference for all path commands with examples, followed by a worked pie chart that puts the arc command through its paces.
The full MDN reference lives at developer.mozilla.org/en-US/docs/Web/SVG/Tutorial/Paths.
Move and Line Commands
M — Move To
M x y
Lifts the pen and moves to (x, y) without drawing. Every path starts with an M. On its own it’s invisible — it’s a positioning command.
<svg viewBox="0 0 200 80">
<path d="M 20 40 L 180 40" stroke="#1098CD" stroke-width="2" fill="none"/>
</svg>
L — Line To
L x y
Draws a straight line from the current point to (x, y).
<svg viewBox="0 0 200 100">
<path d="M 20 80 L 100 20 L 180 80" stroke="#1098CD" stroke-width="2" fill="none"/>
</svg>
H — Horizontal Line
H x
Draws a horizontal line to the given x coordinate, keeping y unchanged. Shorthand for L x currentY.
V — Vertical Line
V y
Draws a vertical line to the given y coordinate, keeping x unchanged. Shorthand for L currentX y.
<svg viewBox="0 0 200 100">
<!-- Grid drawn with H and V commands -->
<path d="M 20 20 H 180 V 80 H 20 V 20" stroke="#1098CD" stroke-width="2" fill="none"/>
</svg>
Z — Close Path
Z
Draws a straight line from the current point back to the start of the path and closes it. No coordinates — it always targets the origin point of the current subpath.
<svg viewBox="0 0 200 100">
<path d="M 100 15 L 180 80 L 20 80 Z" stroke="#1098CD" stroke-width="2" fill="#1098CD" fill-opacity="0.15"/>
</svg>
Curve Commands
C — Cubic Bézier
C x1 y1, x2 y2, x y
Draws a cubic Bézier curve from the current point to (x, y). (x1, y1) is the control point for the start of the curve; (x2, y2) is the control point for the end. The control points pull the curve toward them — think of them as gravity wells.
<svg viewBox="0 0 200 100">
<!-- Control point handles shown as dashed lines -->
<line x1="20" y1="80" x2="60" y2="10" stroke="#ccc" stroke-width="1" stroke-dasharray="4"/>
<line x1="140" y1="10" x2="180" y2="80" stroke="#ccc" stroke-width="1" stroke-dasharray="4"/>
<circle cx="60" cy="10" r="3" fill="#51B0D5"/>
<circle cx="140" cy="10" r="3" fill="#51B0D5"/>
<path d="M 20 80 C 60 10, 140 10, 180 80" stroke="#1098CD" stroke-width="2" fill="none"/>
</svg>
S — Smooth Cubic Bézier
S x2 y2, x y
Shorthand for a cubic Bézier. Omits the first control point — it’s automatically reflected from the previous C or S command’s second control point, producing a smooth join. Use it to chain curves without visible kinks.
<svg viewBox="0 0 300 100">
<path d="M 20 80 C 60 10, 100 10, 140 80 S 220 10, 280 80"
stroke="#1098CD" stroke-width="2" fill="none"/>
</svg>
Q — Quadratic Bézier
Q x1 y1, x y
Like a cubic Bézier but with a single control point shared by both the start and end. Produces a softer, simpler curve — fewer parameters to think about, less fine control. The start, control point, and end form a triangle; the curve is pulled toward the triangle’s apex.
<svg viewBox="0 0 200 100">
<line x1="20" y1="80" x2="100" y2="10" stroke="#ccc" stroke-width="1" stroke-dasharray="4"/>
<line x1="100" y1="10" x2="180" y2="80" stroke="#ccc" stroke-width="1" stroke-dasharray="4"/>
<circle cx="100" cy="10" r="3" fill="#51B0D5"/>
<path d="M 20 80 Q 100 10, 180 80" stroke="#1098CD" stroke-width="2" fill="none"/>
</svg>
T — Smooth Quadratic Bézier
T x y
The quadratic counterpart to S. The control point is automatically reflected from the previous Q or T, making chained smooth waves straightforward to write.
<svg viewBox="0 0 340 100">
<!-- A wave: initial Q sets the first control point,
T chains with reflected control points -->
<path d="M 20 50 Q 60 10, 100 50 T 180 50 T 260 50 T 320 50"
stroke="#1098CD" stroke-width="2" fill="none"/>
</svg>
Arc Command
The arc command is the most powerful and most misunderstood. It draws an elliptical arc between two points.
A rx ry x-rotation large-arc-flag sweep-flag x y
| Parameter | Description |
|---|---|
rx |
X radius of the ellipse |
ry |
Y radius of the ellipse |
x-rotation |
Rotation of the ellipse’s x-axis in degrees (0 for circles) |
large-arc-flag |
0 = take the smaller arc, 1 = take the larger arc |
sweep-flag |
0 = counter-clockwise, 1 = clockwise |
x y |
End point of the arc |
Any two points on an ellipse define four possible arcs — two arc sizes (large and small) in two directions (clockwise and counter-clockwise). The large-arc-flag and sweep-flag together select which of the four you want.
<svg viewBox="0 0 400 200">
<!-- All four arcs between the same two points (50,100) and (350,100) -->
<!-- large-arc=0, sweep=0 (small, counter-clockwise) -->
<path d="M 50 100 A 150 70 0 0 0 350 100" fill="none" stroke="#1098CD" stroke-width="2"/>
<!-- large-arc=0, sweep=1 (small, clockwise) -->
<path d="M 50 100 A 150 70 0 0 1 350 100" fill="none" stroke="#51B0D5" stroke-width="2" stroke-dasharray="6"/>
<!-- large-arc=1, sweep=0 (large, counter-clockwise) -->
<path d="M 50 100 A 150 70 0 1 0 350 100" fill="none" stroke="#06394D" stroke-width="2"/>
<!-- large-arc=1, sweep=1 (large, clockwise) -->
<path d="M 50 100 A 150 70 0 1 1 350 100" fill="none" stroke="#0C729A" stroke-width="2" stroke-dasharray="6"/>
<!-- Start and end markers -->
<circle cx="50" cy="100" r="4" fill="#1098CD"/>
<circle cx="350" cy="100" r="4" fill="#1098CD"/>
<!-- Labels -->
<text x="200" y="60" text-anchor="middle" font-size="11" fill="#06394D">large=1 sweep=0</text>
<text x="200" y="148" text-anchor="middle" font-size="11" fill="#1098CD">large=0 sweep=0</text>
<text x="200" y="162" text-anchor="middle" font-size="11" fill="#51B0D5" fill-opacity="0.8">large=0 sweep=1</text>
<text x="200" y="195" text-anchor="middle" font-size="11" fill="#0C729A">large=1 sweep=1</text>
</svg>
For a circle segment (pie slice), rx and ry are equal and x-rotation is 0. The sweep-flag determines clockwise (1) or counter-clockwise (0); the large-arc-flag determines which arc is taken when the angle exceeds or falls short of 180°.
Command Reference
| Command | Full form | Description |
|---|---|---|
M x y |
Move To | Lifts pen, moves to (x, y) |
L x y |
Line To | Straight line to (x, y) |
H x |
Horizontal Line | Horizontal line to x |
V y |
Vertical Line | Vertical line to y |
Z |
Close Path | Line back to path start |
C x1 y1, x2 y2, x y |
Cubic Bézier | Curve with two control points |
S x2 y2, x y |
Smooth Cubic | Continues cubic; reflects prior control point |
Q x1 y1, x y |
Quadratic Bézier | Curve with one shared control point |
T x y |
Smooth Quadratic | Continues quadratic; reflects prior control point |
A rx ry rot large sweep x y |
Arc | Elliptical arc to (x, y) |
Lowercase versions of all commands (m, l, h, v, z, c, s, q, t, a) use coordinates relative to the current position rather than absolute coordinates in the SVG viewport.
SVG Line Animation
Stroke animation works by exploiting two CSS properties: stroke-dasharray sets the length of dashes and gaps in the stroke; stroke-dashoffset shifts where the dash pattern starts. Setting both to the path length makes the stroke appear invisible — the gap covers the whole path. Animating stroke-dashoffset to 0 slides the dash into view, producing the drawing effect.
path {
stroke-dasharray: 1000;
stroke-dashoffset: 1000;
animation: draw 2s ease forwards;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
In practice, you rarely know the path length in advance. JavaScript provides it:
const path = document.querySelector('path');
const length = path.getTotalLength();
path.style.strokeDasharray = length;
path.style.strokeDashoffset = length;
// Trigger after a frame to ensure the initial state is painted
requestAnimationFrame(() => {
path.style.transition = 'stroke-dashoffset 2s ease';
path.style.strokeDashoffset = '0';
});
The same approach works for partial draws — animate to strokeDashoffset = length * 0.7 to draw 70% of the path.
Worked Example: Pie Chart
A pie chart is a sequence of arc segments radiating from a centre point. Each slice is a path from the centre to the arc’s start point, along the arc, and back to the centre with Z.
The tricky part is the coordinate maths. Given a centre (cx, cy), radius r, and an angle in degrees:
function polarToCartesian(cx, cy, r, angleDeg) {
const rad = (angleDeg - 90) * (Math.PI / 180); // -90 starts at 12 o'clock
return {
x: cx + r * Math.cos(rad),
y: cy + r * Math.sin(rad),
};
}
function slicePath(cx, cy, r, startDeg, endDeg) {
const start = polarToCartesian(cx, cy, r, startDeg);
const end = polarToCartesian(cx, cy, r, endDeg);
const largeArc = endDeg - startDeg > 180 ? 1 : 0;
return [
`M ${cx} ${cy}`,
`L ${start.x.toFixed(1)} ${start.y.toFixed(1)}`,
`A ${r} ${r} 0 ${largeArc} 1 ${end.x.toFixed(1)} ${end.y.toFixed(1)}`,
'Z',
].join(' ');
}
Applied to five slices totalling 360°:
const data = [
{ label: 'Frontend', pct: 35, color: '#1098CD' },
{ label: 'Backend', pct: 25, color: '#51B0D5' },
{ label: 'Infrastructure', pct: 20, color: '#06394D' },
{ label: 'Design', pct: 12, color: '#0C729A' },
{ label: 'Research', pct: 8, color: '#1D404D' },
];
let startDeg = 0;
data.forEach(d => {
const endDeg = startDeg + (d.pct / 100) * 360;
console.log(slicePath(200, 180, 150, startDeg, endDeg));
startDeg = endDeg;
});
The resulting SVG — written directly here to show the path values the maths produces:
<svg viewBox="0 0 600 360" xmlns="http://www.w3.org/2000/svg">
<!-- Frontend 35% — 0° to 126° -->
<path d="M 200 180 L 200 30 A 150 150 0 0 1 321.4 268.2 Z" fill="#1098CD" stroke="white" stroke-width="1.5"/>
<!-- Backend 25% — 126° to 216° -->
<path d="M 200 180 L 321.4 268.2 A 150 150 0 0 1 111.8 301.4 Z" fill="#51B0D5" stroke="white" stroke-width="1.5"/>
<!-- Infrastructure 20% — 216° to 288° -->
<path d="M 200 180 L 111.8 301.4 A 150 150 0 0 1 57.3 133.6 Z" fill="#06394D" stroke="white" stroke-width="1.5"/>
<!-- Design 12% — 288° to 331.2° -->
<path d="M 200 180 L 57.3 133.6 A 150 150 0 0 1 127.7 48.6 Z" fill="#0C729A" stroke="white" stroke-width="1.5"/>
<!-- Research 8% — 331.2° to 360° -->
<path d="M 200 180 L 127.7 48.6 A 150 150 0 0 1 200 30 Z" fill="#1D404D" stroke="white" stroke-width="1.5"/>
</svg>
The large-arc-flag logic is the one part that trips people up. The rule: if a slice spans more than 180° set it to 1, otherwise 0. In the slicePath helper above that’s the single line endDeg - startDeg > 180 ? 1 : 0. Get it wrong and the browser draws the complementary arc — you’ll see a near-full circle where a small wedge should be.
Bonus: Donut Chart
A donut is a pie chart with a circular hole cut out using an inner arc traced in the opposite direction. Each slice becomes a closed region between an outer arc and an inner arc:
function donutSlicePath(cx, cy, outerR, innerR, startDeg, endDeg) {
const outerStart = polarToCartesian(cx, cy, outerR, startDeg);
const outerEnd = polarToCartesian(cx, cy, outerR, endDeg);
const innerStart = polarToCartesian(cx, cy, innerR, endDeg); // note: reversed
const innerEnd = polarToCartesian(cx, cy, innerR, startDeg); // note: reversed
const largeArc = endDeg - startDeg > 180 ? 1 : 0;
return [
`M ${outerStart.x.toFixed(1)} ${outerStart.y.toFixed(1)}`,
`A ${outerR} ${outerR} 0 ${largeArc} 1 ${outerEnd.x.toFixed(1)} ${outerEnd.y.toFixed(1)}`,
`L ${innerStart.x.toFixed(1)} ${innerStart.y.toFixed(1)}`,
`A ${innerR} ${innerR} 0 ${largeArc} 0 ${innerEnd.x.toFixed(1)} ${innerEnd.y.toFixed(1)}`,
'Z',
].join(' ');
}
The outer arc sweeps clockwise (sweep=1), the inner arc sweeps counter-clockwise (sweep=0) back to the start. The fill rule then correctly treats the enclosed ring as the filled area.
The inner arc coordinate maths is the same polarToCartesian function — just applied at innerR instead of outerR, with startDeg and endDeg swapped so the direction reverses cleanly.
Once you have the arc fundamentals, the D3 arc() generator is doing exactly this — it’s the same maths wrapped in an API. Reading paths by hand first makes the generated output legible rather than opaque.