SVG Essentials: Mastering the Path Element
If basic shapes like circles and rectangles are the "Lego bricks" of SVG, the <path> element is the professional pen tool. It is
a single, ultra-powerful element capable of drawing any shape imaginable — from custom icons to fluid, organic illustrations — by following
a string of specific commands.
Before we dive into the "alphabet" of path data, it is important to have a solid grasp of how the SVG canvas works. If you aren't yet comfortable with the top-left coordinate system or the logic of the viewBox, I recommend starting there first.
Once you have the grid down, you're ready to master the d attribute and move from simple geometry to advanced vector mastery.
The d Attribute: The Map of Your Shape
The <path> element relies almost entirely on one attribute: d (which stands for data). This attribute contains a series of
commands and coordinates that tell the browser exactly where to move the "virtual pen."
Example of simple triangle:
<path d="M10 80 L100 20 L190 80 Z" fill="lightblue" stroke="blue" stroke-width="2"/>Absolute vs. Relative Coordinates
This is one of the most important "secrets" of SVG paths. The casing of the letter command changes how the browser interprets the numbers:
- UPPERCASE (e.g.,
L 50 50): Absolute coordinates. "Draw a line to exactly the coordinate (50, 50) on the grid." - lowercase (e.g.,
l 50 50): Relative coordinates. "Draw a line 50 units right and 50 units down from where the pen currently is."
Why professionals prefer relative commands:
- Easier manual editing
- Cleaner exports
- Better compression
Moving and Drawing Lines
Straight-line commands form the structural skeleton of every SVG path. Even the most complex icons are built on these primitives before curves are added.
MoveTo (M / m)
The MoveTo command positions the pen without drawing.
Every path must begin with a move command.
If multiple coordinate pairs are provided, the first pair moves the pen and the rest are treated as implicit LineTo commands.
Syntax:
- Absolute:
M x y- move to absolute coordinates(x,y) - Relative:
m dx dy- move relative to current position
Mental model:
Lift the pen, move to a location, don't draw.
Example:
Path Command: M (MoveTo)
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- Coordinate guides -->
<line x1="20" y1="0" x2="20" y2="100" stroke="var(--color-border-default)" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.45" />
<line x1="0" y1="20" x2="100" y2="20" stroke="var(--color-border-default)" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.45" />
<!-- MoveTo point -->
<circle cx="20" cy="20" r="2" fill="#6366f1" opacity="0.2" />
<circle cx="20" cy="20" r="1" fill="#6366f1" />
<!-- M command -->
<path d="M 20 20" fill="none" stroke="#6366f1" stroke-width="1" />
</svg>LineTo (L / l)
Draws a straight line from the current position to the specified point.
Syntax:
- Absolute:
L x y - Relative:
l dx dy
Implicit Behavior:
M10 10 L20 20 30 30 40 40 draws three lines without repeating L — once a command letter is set, extra coordinate pairs keep reusing it.
Example:
Path Command: L (LineTo)
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- Coordinate guides -->
<line x1="10" y1="10" x2="90" y2="10" stroke="var(--color-border-default)" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.5" />
<line x1="90" y1="10" x2="90" y2="90" stroke="var(--color-border-default)" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.5" />
<!-- Main path -->
<path d="M 10 10 L 90 90" fill="none" stroke="#6366f1" stroke-width="1.5" stroke-linecap="round" />
<!-- Start point -->
<circle cx="10" cy="10" r="2" fill="#6366f1" opacity="0.2" />
<circle cx="10" cy="10" r="1" fill="#6366f1" />
<!-- End point -->
<circle cx="90" cy="90" r="2" fill="#6366f1" opacity="0.2" />
<circle cx="90" cy="90" r="1" fill="#6366f1" />
</svg>Horizontal LineTo (H / h)
Draws a horizontal line while keeping the current y value unchanged.
Syntax:
- Absolute:
H x - Relative:
h dx
When to use:
- Pixel-perfect edges
- UI shapes
- Charts and grids
Example:
Path Command: H (Horizontal LineTo)
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- Horizontal construction guide -->
<line x1="10" y1="50" x2="90" y2="50" stroke="var(--color-border-default)" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.5" />
<!-- Vertical guide to endpoint -->
<line x1="90" y1="20" x2="90" y2="80" stroke="var(--color-border-default)" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.4" />
<path d="M 10 50 H 90" fill="none" stroke="#10b981" stroke-width="1.5" stroke-linecap="round" />
<!-- Start -->
<circle cx="10" cy="50" r="1" fill="var(--color-fg-muted)" />
<!-- End -->
<circle cx="90" cy="50" r="2" fill="#10b981" opacity="0.2" />
<circle cx="90" cy="50" r="1" fill="#10b981" />
</svg>Vertical LineTo (V / v)
Draws a vertical line while keeping the current x value unchanged.
Syntax:
- Absolute:
V y - Relative:
v dy
Example:
Path Command: V (Vertical LineTo)
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- Vertical construction guide -->
<line x1="50" y1="10" x2="50" y2="90" stroke="var(--color-border-default)" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.5" />
<!-- Horizontal guide -->
<line x1="20" y1="90" x2="80" y2="90" stroke="var(--color-border-default)" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.4" />
<path d="M 50 10 V 90" fill="none" stroke="#f43f5e" stroke-width="1.5" stroke-linecap="round" />
<!-- Start -->
<circle cx="50" cy="10" r="1" fill="var(--color-fg-muted)" />
<!-- End -->
<circle cx="50" cy="90" r="2" fill="#f43f5e" opacity="0.2" />
<circle cx="50" cy="90" r="1" fill="#f43f5e" />
</svg>ClosePath (Z / z)
Closes the current sub-path by drawing a straight line back to the starting point of the last M.
- No coordinates required
- Resets the current point
- Essential for filled shapes
z and Z perform the exact same action, unlike other commands.
Mental model:
Example:
Path Command: Z (ClosePath)
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- Closing edge guide -->
<line x1="80" y1="80" x2="20" y2="80" stroke="var(--color-border-default)" stroke-width="0.75" stroke-dasharray="2 2" opacity="0.6" />
<!-- One L, two coordinate pairs — the second "L" is implicit, per the post's shorthand tricks. -->
<path d="M 20 80 L 50 20 80 80 Z" fill="#6366f1" fill-opacity="0.2" stroke="#6366f1" stroke-width="1.5" stroke-linejoin="round" />
<!-- Start / close point -->
<circle cx="20" cy="80" r="2" fill="#6366f1" opacity="0.2" />
<circle cx="20" cy="80" r="1" fill="#6366f1" />
<!-- Peak -->
<circle cx="50" cy="20" r="2" fill="#6366f1" opacity="0.2" />
<circle cx="50" cy="20" r="1" fill="#6366f1" />
<!-- End -->
<circle cx="80" cy="80" r="1" fill="var(--color-fg-muted)" />
</svg>Mastering the Curves
Straight lines define structure, but curves define personality. SVG paths support bézier curves and arcs, which allow smooth, organic, and professional-looking shapes.
SVG uses two types of bézier curves and elliptical arcs.
Quadratic Bézier (Q / q)
A Quadratic Bézier curve is defined using one control point and one end point. The curve bends toward the control point but never passes through it.
This type of curve is simpler and lighter than cubic béziers, making it useful for gentle curves and wave-like shapes.
Syntax:
- Absolute:
Q x1 y1, x y(Control point, End point) - Relative:
q dx1 dy1 dx dy
Mental model:
- Start point → curve bends toward control point → ends at end point
Example:
Path Command: Q (Quadratic Bézier)
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- Bézier control skeleton -->
<path d="M 10 50 L 50 10 90 50" fill="none" stroke="var(--color-border-default)" stroke-width="0.75" stroke-dasharray="2 2" opacity="0.7" />
<!-- Main curve -->
<path d="M 10 50 Q 50 10 90 50" fill="none" stroke="#ec4899" stroke-width="1.5" stroke-linecap="round" />
<!-- Start -->
<circle cx="10" cy="50" r="1" fill="var(--color-fg-muted)" />
<!-- Control point -->
<circle cx="50" cy="10" r="2" fill="#ec4899" opacity="0.2" />
<circle cx="50" cy="10" r="1" fill="#ec4899" />
<!-- End -->
<circle cx="90" cy="50" r="1" fill="var(--color-fg-muted)" />
</svg>Smooth Quadratic Bézier (T / t)
The T command is a shortcut that continues a quadratic curve smoothly.
It automatically reflects the previous control point, so you only specify the new end point.
Syntax:
- Absolute:
T x y - Relative:
t dx dy
Why it matters:
- Perfect for waves, flowing strokes, and handwriting-style paths
- Eliminates repeated control-point math
Example:
Path Command: T (Smooth Quadratic Bézier)
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- First quadratic skeleton -->
<path d="M 10 50 L 30 10 50 50" fill="none" stroke="var(--color-border-default)" stroke-width="0.75" stroke-dasharray="2 2" opacity="0.7" />
<!-- Reflected control skeleton -->
<path d="M 50 50 L 70 90 90 50" fill="none" stroke="var(--color-fg-accent)" stroke-width="0.75" stroke-dasharray="2 2" opacity="0.6" />
<!-- Main path -->
<path d="M 10 50 Q 30 10 50 50 T 90 50" fill="none" stroke="#ec4899" stroke-width="1.5" stroke-linecap="round" />
<!-- Start -->
<circle cx="10" cy="50" r="1" fill="var(--color-fg-muted)" />
<!-- Original control -->
<circle cx="30" cy="10" r="2" fill="#ec4899" opacity="0.2" />
<circle cx="30" cy="10" r="1" fill="#ec4899" />
<!-- Junction -->
<circle cx="50" cy="50" r="2" fill="var(--color-fg-muted)" opacity="0.2" />
<circle cx="50" cy="50" r="1" fill="var(--color-fg-muted)" />
<!-- Reflected/implicit T control -->
<circle cx="70" cy="90" r="2" fill="none" stroke="#ec4899" stroke-width="0.75" stroke-dasharray="2 2" />
<!-- End -->
<circle cx="90" cy="50" r="1" fill="var(--color-fg-muted)" />
</svg>Cubic Bézier (C / c)
Cubic Bézier curves are the most powerful and commonly used curve type.
They use two control points, allowing complex S curves and precise shaping.
This is the curve type used by design tools like Figma, Illustrator, and Sketch.
Syntax:
- Absolute:
C x1 y1 x2 y2 x y(First control, Second control, End point) - Relative:
c dx1 dy1 dx2 dy2 dx dy
Mental model:
- First control point controls the curve's entry direction
- Second control point controls the curve's exit direction
Example:
Path Command: C (Cubic Bézier)
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- First control handle -->
<line x1="10" y1="50" x2="30" y2="10" stroke="var(--color-border-default)" stroke-width="0.75" stroke-dasharray="2 2" opacity="0.7" />
<!-- Second control handle -->
<line x1="90" y1="50" x2="70" y2="90" stroke="var(--color-border-default)" stroke-width="0.75" stroke-dasharray="2 2" opacity="0.7" />
<!-- Main curve -->
<path d="M 10 50 C 30 10 70 90 90 50" fill="none" stroke="#6366f1" stroke-width="1.5" stroke-linecap="round" />
<!-- Start -->
<circle cx="10" cy="50" r="1" fill="var(--color-fg-muted)" />
<!-- C1 -->
<circle cx="30" cy="10" r="2" fill="#6366f1" opacity="0.2" />
<circle cx="30" cy="10" r="1" fill="#6366f1" />
<!-- C2 -->
<circle cx="70" cy="90" r="2" fill="#6366f1" opacity="0.2" />
<circle cx="70" cy="90" r="1" fill="#6366f1" />
<!-- End -->
<circle cx="90" cy="50" r="1" fill="var(--color-fg-muted)" />
</svg>Smooth Cubic Bézier (S / s)
The S command creates a smooth continuation of a cubic bézier curve.
It automatically reflects the second control point of the previous C or S command, so only one control point is required.
Syntax:
- Absolute:
S x2 y2 x y - Relative:
s dx2 dy2 dx dy
Why it matters:
- Reduces path complexity
- Ideal for flowing icons and symmetrical curves
Example:
Path Command: S (Smooth Cubic Bézier)
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- Previous cubic control skeleton -->
<path d="M 10 50 L 20 10 40 10 50 50" fill="none" stroke="var(--color-border-default)" stroke-width="0.75" stroke-dasharray="2 2" opacity="0.7" />
<!-- Smooth reflected control -->
<path d="M 50 50 L 60 90 80 90 90 50" fill="none" stroke="var(--color-fg-accent)" stroke-width="0.75" stroke-dasharray="2 2" opacity="0.65" />
<!-- Main path -->
<path d="M 10 50 C 20 10 40 10 50 50 S 80 90 90 50" fill="none" stroke="#6366f1" stroke-width="1.5" stroke-linecap="round" />
<!-- Start -->
<circle cx="10" cy="50" r="1" fill="var(--color-fg-muted)" />
<!-- Previous C2 -->
<circle cx="40" cy="10" r="2" fill="#6366f1" opacity="0.2" />
<circle cx="40" cy="10" r="1" fill="#6366f1" />
<!-- Junction -->
<circle cx="50" cy="50" r="2" fill="var(--color-fg-muted)" opacity="0.2" />
<circle cx="50" cy="50" r="1" fill="var(--color-fg-muted)" />
<!-- Implicit reflected control -->
<circle cx="60" cy="90" r="2" fill="none" stroke="#6366f1" stroke-width="0.75" stroke-dasharray="2 2" />
<!-- S second control -->
<circle cx="80" cy="90" r="2" fill="#6366f1" opacity="0.2" />
<circle cx="80" cy="90" r="1" fill="#6366f1" />
<!-- End -->
<circle cx="90" cy="50" r="1" fill="var(--color-fg-muted)" />
</svg>Elliptical Arc (A / a)
The Arc command draws a portion of an ellipse between two points. It is commonly used for circles, rounded corners, and curved UI shapes.
Syntax:
- Absolute:
A rx ry x-axis-rotation large-arc-flag sweep-flag x y - Relative:
a rx ry x-axis-rotation large-arc-flag sweep-flag dx dy
| Parameter | Meaning |
|---|---|
rx ry | X and Y radius of the ellipse |
x-axis-rotation | Rotation of ellipse (degrees) |
large-arc-flag | 0 = small arc, 1 = large arc |
sweep-flag | 0 = counter-clockwise, 1 = clockwise |
x y | End point of the arc |
Example:
Path Command: A (Elliptical Arc)
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- Approximate ellipse used to visualize rx / ry -->
<ellipse cx="50" cy="50" rx="30" ry="20" fill="none" stroke="var(--color-border-default)" stroke-width="0.75" stroke-dasharray="2 2" opacity="0.55" transform="rotate(0 50 50)" />
<!-- Start-to-end chord -->
<line x1="20" y1="50" x2="80" y2="50" stroke="var(--color-border-default)" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.45" />
<!-- Arc: flags written back-to-back (e.g. "01" instead of "0 1") — see the Path Element post's shorthand tricks. -->
<path d="M 20 50 A 30 20 0 01 80 50" fill="none" stroke="#10b981" stroke-width="1.5" stroke-linecap="round" />
<!-- Start point -->
<circle cx="20" cy="50" r="2" fill="#10b981" opacity="0.2" />
<circle cx="20" cy="50" r="1" fill="#10b981" />
<!-- End point -->
<circle cx="80" cy="50" r="2" fill="#10b981" opacity="0.2" />
<circle cx="80" cy="50" r="1" fill="#10b981" />
<!-- Arc midpoint / ellipse center -->
<circle cx="50" cy="50" r="1" fill="var(--color-fg-muted)" />
</svg>Why arcs feel complex:
- Flags (0 / 1) control which arc is drawn
- SVG chooses between four possible arcs
Seeing All Four Arcs
Any two points on a circle's edge can be connected in four different ways — short or long way around, curving clockwise or counter-clockwise. The two flags each pick one of those two choices:
large-arc-flag | sweep-flag | Result |
|---|---|---|
0 | 0 | Short way, counter-clockwise |
0 | 1 | Short way, clockwise |
1 | 0 | Long way, counter-clockwise |
1 | 1 | Long way, clockwise |
Most "why did my arc go the wrong way" bugs come down to sweep-flag alone, since it's the one that decides direction rather than distance — if the curve bulges the wrong way, flip that flag first before touching anything else.
Path Commands Overview
Here is a complete list of path commands:
| Command | Type | Coordinates | Description |
|---|---|---|---|
M / m | MoveTo | x y | Moves cursor without drawing. |
L / l | LineTo | x y | Draws a line to given point |
H / h | Horizontal LineTo | x | Draws horizontal line |
V / v | Vertical LineTo | y | Draws vertical line |
Z / z | ClosePath | - | Closes path by connecting to start |
Q / q | Quadratic Bézier | x1 y1 x y | Curve with 1 control point |
T / t | Smooth Quadratic Bézier | x y | Quadratic with control point inferred |
C / c | Cubic Bézier | x1 y1 x2 y2 x y | Curve with 2 control points |
S / s | Smooth Cubic Bézier | x2 y2 x y | Cubic with first control point inferred |
A / a | Elliptical Arc | rx ry x-axis-rotation large-arc-flag sweep-flag x y | Draws elliptical arc |
Geometry Rules at a Glance
Just like the basic shapes' geometry rules, <path> has its own set of constraints — scattered across the sections above, but worth compiling into one glance-able reference:
| Concept | Key geometry | Rule to remember |
|---|---|---|
d attribute | starts with M / m | Every path must begin with a move — there's no implicit starting point the way <rect> has x/y |
Z / z | closes to the last M | Determines open vs. closed, the same role as polyline vs. polygon: unclosed still fills fine, but strokes with a visible gap at the seam |
A / a | rx, ry | A zero radius collapses the arc into a straight line; the flags — not the radii — pick which of the 4 possible arcs is drawn |
| Compound sub-paths | multiple Ms in one d | fill-rule decides which regions count as "inside," not the order or position of the sub-paths |
pathLength | overrides the path's own length units | The one attribute that redefines a path's geometry in the abstract — every other shape's geometry is exactly what it says |
Tips for Using Paths
- Start with MoveTo (
M). - Use
Zto close shapes. - Prefer
HandVfor straight edges. - Plan control points for curves visually.
- Use relative commands for repeated patterns.
- Combine arcs, lines, and curves for icons or illustrations.
- Keep strokes visible during debugging (
fill="none").
Combining Commands
Paths allow multiple commands in one d attribute:
<path d="M20 80 Q100 10, 180 80 T340 80" stroke="purple" fill="none"/>- Mix of move, line, curve, arc
- Creates complex shapes in a single element
When you look at a complex path exported from a design tool, it can look like gibberish at first. Break it down step-by-step, the same way as any other:
- M20 80: Move to (20, 80) — the wave's starting point.
- Q100 10, 180 80: Curve up toward (100, 10) and land at (180, 80) — the first hump.
- T340 80: Smoothly continue the curve to (340, 80) —
Tinfers the control point by reflectingQ's, so the second hump mirrors the first without a control point ever being typed twice.
This is the actual shape of most hand-authored wave and blob paths: one Q to establish the curve, then a chain of Ts to repeat it.
Compound Paths and the fill-rule Property
A single d attribute isn't limited to one closed shape — starting a new M mid-string begins a new sub-path without ending the element. This is how a donut, a letter "O", or an icon with a cut-out hole gets built: two closed sub-paths in one <path>, the second sitting inside the first.
The catch is that the browser then has to decide which regions count as "inside" the shape, and that decision is controlled entirely by fill-rule. Both squares below share the exact same d — only fill-rule differs:
<svg viewBox="0 0 100 100" width="180" height="180">
<path d="M10 10 H90 V90 H10 Z M30 30 H70 V70 H30 Z" fill="#6366f1" fill-rule="nonzero" />
</svg><svg viewBox="0 0 100 100" width="180" height="180">
<path d="M10 10 H90 V90 H10 Z M30 30 H70 V70 H30 Z" fill="#6366f1" fill-rule="evenodd" />
</svg>nonzero(the default): both sub-paths above are drawn in the same winding direction (clockwise), so the inner square's winding count never returns to zero — it gets filled in too, producing a solid square with no hole.evenodd: counts how many sub-path boundaries a ray from that point crosses on its way out. The inner square's interior crosses two boundaries (even), so it's treated as outside the fill — punching a hole regardless of which direction either sub-path winds.
Practical Example: Heart Shape
Practical Example: The Heart Shape
<svg viewBox="0 0 100 100" width="240" height="240">
<!-- Construction guides -->
<path d="M 90 35 L 90 65 50 95 10 65 10 35" fill="none" stroke="var(--color-fg-accent)" stroke-width="0.75" stroke-dasharray="2 2" opacity="0.45" />
<!-- Horizontal center guide -->
<line x1="10" y1="35" x2="90" y2="35" stroke="var(--color-border-default)" stroke-width="0.5" stroke-dasharray="2 2" opacity="0.4" />
<!--
Heart: both arcs share the same rx/ry/rotation/flags, so the second "A" is implicit — one A,
14 numbers (7 per arc) — plus the flags themselves jammed together ("01" instead of "0 1").
Both are the shorthand covered in the post's Common Pitfalls section.
-->
<path d="M 10 35 A 20 20 0 01 50 35 20 20 0 01 90 35 Q 90 65 50 95 Q 10 65 10 35 Z" fill="#f43f5e" fill-opacity="0.2" stroke="#e11d48" stroke-width="1.5" stroke-linejoin="round" />
<!-- Left hump center -->
<circle cx="30" cy="35" r="1" fill="var(--color-fg-muted)" />
<!-- Right hump center -->
<circle cx="70" cy="35" r="1" fill="var(--color-fg-muted)" />
<!-- Left bulge control -->
<circle cx="90" cy="65" r="2" fill="#e11d48" opacity="0.2" />
<circle cx="90" cy="65" r="1" fill="#e11d48" />
<!-- Right bulge control -->
<circle cx="10" cy="65" r="2" fill="#e11d48" opacity="0.2" />
<circle cx="10" cy="65" r="1" fill="#e11d48" />
<!-- Tip -->
<circle cx="50" cy="95" r="2" fill="#e11d48" opacity="0.2" />
<circle cx="50" cy="95" r="1" fill="#e11d48" />
<!-- Start point -->
<circle cx="10" cy="35" r="1" fill="var(--color-fg-muted)" />
<!-- End / close point -->
<circle cx="10" cy="35" r="1.5" fill="var(--color-fg-muted)" />
</svg>- Combines arcs (A) + quadratic béziers (Q)
- Closed path forms a heart shape
At its default settings, the demo above is drawing this d string:
M 10 35 A 20 20 0 01 50 35 20 20 0 01 90 35 Q 90 65 50 95 Q 10 65 10 35 ZTwo shorthand tricks from the pitfalls section are already at work here: the arc flags are jammed together (01 instead of 0 1), and there's only one A even though the heart clearly has two arcs.
Decoded piece by piece, the same way we broke down the wave earlier:
- M 10 35: Start at the top of the left hump.
- A 20 20 0 01 50 35: The first arc, over to (50, 35) — the dip between the two humps. The jammed flags read as
large-arc-flag: 0, sweep-flag: 1; that1curves it downward into the dip rather than up and over. - 20 20 0 01 90 35: No letter here — this is the second arc, implicit. It reuses the previous command (
A) with the exact same 7-value shape (rx ry rotation flags), just a new end point: the top of the right hump. Both humps are identical arcs, which is exactly the case implicit repetition is for. - Q 90 65 50 95: Curve from the right hump down to the bottom point, bulging outward through the control point at (90, 65).
- Q 10 65 10 35: Mirror that curve back up to the starting point, closing the left side.
- Z: Close the path — technically redundant here since the last point already equals the first, but it's what tells the renderer this is a fillable closed shape rather than an open stroke.
Two arcs for the humps (one spelled out, one implicit), two quadratic curves for the sides — that's the entire heart.
Animating Paths: pathLength and the Line-Draw Setup
The classic "line drawing itself" effect — an icon's stroke appearing to trace itself in on scroll or on load — is built entirely from two stroke properties, stroke-dasharray and stroke-dashoffset, plus one attribute that makes the math trivial: pathLength.
By default, stroke-dasharray's unit is whatever unit the path's own geometry uses, which means the "total length" you need for the dash array depends on the exact shape — a different number for every path. Setting pathLength="100" on the <path> overrides that: the browser rescales its internal length calculations so the path is always exactly 100 units long, no matter its actual size or complexity.
<svg viewBox="0 0 100 100" width="200" height="200">
<style>
.draw-me {
stroke-dasharray: 100;
stroke-dashoffset: 100;
animation: draw 2.5s ease forwards infinite;
}
@keyframes draw {
to {
stroke-dashoffset: 0;
}
}
</style>
<path
class="draw-me"
pathLength="100"
d="M20 55 L40 75 80 25"
fill="none"
stroke="#10b981"
stroke-width="6"
stroke-linecap="round"
/>
</svg>With pathLength="100", the recipe is always the same regardless of the path's own coordinates:
stroke-dasharray: 100— one dash exactly as long as the whole path, with a gap after it also 100 units long.stroke-dashoffset: 100— shifts that dash fully off the start, so nothing is visible yet.- Animating
stroke-dashoffsetdown to0slides the dash back into place, revealing the stroke as it goes.
Paths Beyond <svg>: the CSS path() Function
Path syntax isn't limited to the <path> element anymore. The CSS path() function accepts the exact same d-attribute syntax and can drive two properties directly on any HTML or SVG element, with no <svg> wrapper required:
.custom-clip {
/* Same command letters as the d attribute — clips this element to a pentagon shape. */
clip-path: path("M110 15 L210 90 L172 210 L48 210 L10 90 Z");
}
.moves-along-a-curve {
/* Slides the element along the curve instead of clipping to it. */
offset-path: path("M10 80 Q95 10 180 80");
offset-distance: 0%;
animation: travel 3s linear infinite;
}
@keyframes travel {
to {
offset-distance: 100%;
}
}clip-path: path(...)— clips an element to an arbitrary shape without needing an actual<svg>in the markup at all.offset-path: path(...)— moves an element along that path asoffset-distanceanimates from0%to100%, withoffset-rotateoptionally turning the element to face the direction of travel.
Everything learned about commands, curves, and arcs in this post transfers directly — a path() string is read exactly like a d attribute.
Stroke Rendering Details
Two properties decide how a stroked path actually looks at its ends and corners — easy to miss since they only become visible once stroke-width is thick enough to notice. Both apply to any stroked shape, not just <path> — the basic shapes post has a hands-on playground for both if you want to see them toggled live on a simple line and corner first:
stroke-linecap(butt/round/square) — how an open path's two loose ends are capped.butt(the default) stops exactly at the endpoint;roundandsquareboth extend past it by half the stroke width,roundwith a curve andsquarewith a flat edge.stroke-linejoin(miter/round/bevel) — how two segments meet at a corner.miter(the default) comes to a sharp point that can spike dramatically on very acute angles;roundandbevelboth cap that spike, rounded or flat-cut respectively.
<svg viewBox="0 0 260 60" width="390" height="90">
<path d="M20 45 L20 15" fill="none" stroke="#6366f1" stroke-width="12" stroke-linecap="butt" />
<path d="M100 45 L100 15" fill="none" stroke="#6366f1" stroke-width="12" stroke-linecap="round" />
<path d="M180 45 L180 15" fill="none" stroke="#6366f1" stroke-width="12" stroke-linecap="square" />
</svg>Both properties are CSS as well as SVG presentation attributes, so they're just as reachable from a stylesheet as from the markup itself. One more attribute worth knowing for icons that get resized: vector-effect="non-scaling-stroke" keeps stroke-width at a constant pixel thickness even when the element itself is scaled up or down — without it, a scaled-up icon's stroke scales up right along with the shape, which is rarely what's wanted for UI iconography.
Common Pitfalls and Shorthand Tricks
A handful of small rules explain almost every "why doesn't my path render right" moment — and, in the other direction, why exported path data so rarely has a space in it:
- Forgetting
Z: an unclosed path can still be filled (the renderer implicitly closes it for fill purposes), but its stroke will show a visible gap at the seam. If the outline looks broken but the fill looks fine, this is almost always why. - Mixing absolute and relative mid-path: perfectly legal —
M10 10 l20 0 L50 50moves relative for one command and absolute for the next — but it's the easiest way to lose track of where the pen actually is. Stick to one style per path unless there's a specific reason to mix. - Arc flags, not radius, are usually the bug: as covered above, a wrong-looking arc is almost always a
sweep-flagproblem, not anrx/ryproblem.
And the reverse side of the same coin — the shorthand rules that let tools (and hand-written paths) drop almost every space and letter:
- Implicit command repetition: repeat coordinate pairs after any command, not just
L, to reuse the last command letter — it works forM(whose extra pairs become implicitLs, as covered earlier) just as much as forC,Q, and the rest. - No space needed before a negative number:
M10-80parses fine, since-is itself a valid number separator. - Chained decimals:
M0.5.5is read as0.5 0.5— the second.is unambiguous, so it's treated as the start of a new number. - A leading zero can be dropped:
.5works exactly like0.5. - Flags can be jammed together: since
large-arc-flagandsweep-flagare always single digits,A5 5 0 004 4is valid — no separator is needed between0,0, and the coordinate that follows.
None of this is required reading to write paths by hand — but it's exactly why a design tool's export looks like an unbroken wall of characters, and now it's decodable instead of intimidating.
Production Notes: Optimization, Accessibility, and Tools
A few habits separate a path that merely works from one that's ready to ship:
- Trim the precision. Design-tool exports routinely carry 6+ decimal places per coordinate — far more precision than a screen can render. Tools like SVGO collapse that down (commonly to 2–3 decimals), strip redundant commands, and can convert verbose absolute chains into shorter relative ones automatically.
- Label icons for screen readers. A
<path>alone is invisible to assistive tech. Give a meaningful icon a<title>element as its first child (<svg role="img"><title>Search</title><path .../></svg>), or mark purely decorative iconsaria-hidden="true"so screen readers skip them entirely. - Reach for a visual tool before hand-typing complex data. Everything decoded in this post — commands, curves, arc flags — is exactly what a drag-and-drop path editor builds for you: drag points, watch the
dstring update live, and export straight to<path>, standalone SVG, or React.
Summary
While we used <rect> and <circle> in the previous post for simple geometry,
you now have the power to create any custom, organic shape using a single <path>.
The <path> element is arguably the most efficient way to store complex visual data. While we rarely code massive paths by hand, understanding these commands allows you to:
- Optimize: Clean up messy code exported from design tools.
- Animate: Use CSS to animate the
stroke-dashoffsetor change thedattribute values via JavaScript. - Fix Layouts: Quickly adjust a coordinate without reopening a design app.
Getting ten interactive command demos, a full heart-shape decode, and the fill-rule, pathLength, and CSS path() sections to all click together took a fair share of iteration, and I hope it leaves you able to read — and write — a `d` string without flinching. Thanks for sticking with me through the whole alphabet of path commands — go draw something only you could have coded. ✏️