Skip to main content

Harshal V. LADHE

SVG Polygon vs Polyline: Differences, Fill Rules, and Use Cases

Same points, open or closed: fill rules, miters, sparklines.
Published at:
Last updated:
Estimated reading time:29 min read

Introduction

In SVG Essentials: Mastering Shapes, Coordinates, and Styling, we covered the six core SVG shapes and the coordinate system they all share. Two of those elements, <polygon> and <polyline>, deserve a closer look on their own: they take an identical points syntax and render it in two completely different ways.

This post starts with a short recap and then goes well past the basics:

  • how fill rules actually decide what's "inside", with a ray-crossing walkthrough;
  • why a polyline that repeats its first point still isn't a polygon;
  • how stroke-miterlimit quietly bevels sharp star tips;
  • generating points with JavaScript, drawing sparklines, and putting markers on vertices;
  • animating a line drawing itself in, and morphing one shape into another;
  • the performance, accessibility and "time to switch to <path>" decisions that come up in real charts, icons and route maps.

Polygon vs Polyline at a Glance

Feature<polyline><polygon>
Shape typeOpenClosed
Final segmentNot drawn; the stroke stops at the last pointDrawn automatically back to the first point
Fill behaviorStill filled by default (fill is black), so usually set fill="none"Filled by default, which is usually what you want
stroke-linecapVisible at both loose endsNo loose ends (unless the stroke is dashed)
Best use casesCharts, sparklines, waveforms, routesGeometric shapes, UI icons, stars, badges

Neither element can draw a curve. For curves, arcs or holes, you need <path>; see When to Switch to <path> below.

Example Comparison

The two shapes below use the exact same points string. Only the element differs, and that one swap is the whole difference between an open line and a closed, filled shape.

<svg viewBox="0 0 220 110" width="440" height="220">
  <g>
    <polyline points="10,80 30,20 50,60 70,20 90,80" fill="none" stroke="#3b82f6" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
    <text x="50" y="105" font-size="8" text-anchor="middle" fill="currentColor">polyline (open)</text>
  </g>
  <g transform="translate(120,0)">
    <polygon points="10,80 30,20 50,60 70,20 90,80" fill="#3b82f6" fill-opacity="0.3" stroke="#3b82f6" stroke-width="2" stroke-linejoin="round" />
    <text x="50" y="105" font-size="8" text-anchor="middle" fill="currentColor">polygon (closed)</text>
  </g>
</svg>
polyline (open) polygon (closed)

Under the hood, both elements work out their fill the same way. The browser treats the shape as closed, with an imaginary segment from the last point back to the first. What differs is the stroke: <polygon> actually strokes that closing segment and joins it neatly to the first one, while <polyline> leaves it unstroked. That's why a polyline without fill="none" still paints a solid region:

Open polyline versus closed polygon from the same pointsThe same five points drawn twice. On the left, a polyline with no fill set: its stroke stops at the last point, but the region between the line and a dashed, unstroked chord from the last point back to the first is still shaded, because fill defaults to black. On the right, a polygon: the browser adds that same closing segment, strokes it, and joins it to the first segment, producing a sealed outline.<polyline>, fill not setfill still defaults to blackphantom fill boundary: filled, never strokedstartend<polygon>closes itself back to the startauto closing segment: stroked and joinedstartendidentical points attribute: only the element name changes

Quick Recap: points, <polyline>, and <polygon>

If you've read the basic shapes post, this section is a refresher. Otherwise, it's everything you need for the rest of this post.

The points attribute. Both elements take a list of X/Y pairs in user units (no px). Each pair is one vertex, and the vertices are connected in the order they're written. Commas and whitespace are interchangeable separators, so these three are equivalent. The first is the most readable:

<polyline points="10,10 50,60 90,20" />  <!-- recommended: comma inside a pair, space between pairs -->
<polyline points="10 10 50 60 90 20" />
<polyline points="10 10, 50 60, 90 20" />

Malformed lists. An odd number of values leaves a dangling coordinate with no partner. The spec treats that as an error. In practice, engines usually render the complete pairs and ignore the leftover number, but error handling has changed between spec versions, so treat an odd count as a bug to fix rather than behaviour to rely on. An empty list, or one with a single point, has no segment to draw and renders nothing.

<polyline> connects the points and stops at the last one. Because fill defaults to black, always add fill="none" unless you really want the region shaded:

<polyline points="10,80 40,20 70,60 100,30" stroke="blue" />              <!-- phantom black fill -->
<polyline points="10,80 40,20 70,60 100,30" stroke="blue" fill="none" />  <!-- just the line -->

<polygon> connects the points and then seals the shape with a segment from the last point back to the first:

<polygon points="50,10 90,80 10,80" fill="lightblue" stroke="navy" stroke-width="2" />

Explicit Closing: A Repeated Point Isn't a Polygon

A common shortcut is to "close" a polyline by repeating its first point at the end. The outline looks closed, but it isn't the same as a <polygon>. The seam where the two ends meet is drawn as two separate line ends, not as a corner, so stroke-linecap applies there and stroke-linejoin doesn't.

With the default butt caps and a thick stroke, that shows up as a notch at the starting corner:

<svg viewBox="0 0 220 110" width="440" height="220">
  <g>
    <polyline points="20,20 80,20 80,80 20,80 20,20" fill="none" stroke="#f43f5e" stroke-width="12" />
    <text x="50" y="105" font-size="8" text-anchor="middle" fill="currentColor">polyline, first point repeated</text>
  </g>
  <g transform="translate(110,0)">
    <polygon points="20,20 80,20 80,80 20,80" fill="none" stroke="#10b981" stroke-width="12" />
    <text x="50" y="105" font-size="8" text-anchor="middle" fill="currentColor">polygon</text>
  </g>
</svg>
polyline, first point repeated polygon

Look at the top-left corner of the red square. The first segment's butt cap and the last segment's butt cap both stop flush at 20,20, so the outer corner square is never painted. The polygon draws a real miter join there, just like its other three corners. stroke-linecap="square" happens to fill the gap at a 90° corner, but not at any other angle, so it's a coincidence rather than a fix.

The seam affects more than caps:

  • Dashes: on the polyline, a dash that would continue across the seam is cut into two capped ends instead of turning the corner with a join. In both versions the dash pattern starts at the first point, so the seam can show a partial dash. You can hide it by making the total length a whole number of pattern repeats, which pathLength makes easy.
  • Markers: in both versions, marker-start and marker-end land on the same spot, the first point. The difference is orientation: with orient="auto", a polygon's start and end markers point along the bisector of the closing and first segments, while the polyline's start marker follows only the first segment and its end marker only the last.
  • Length: both versions include the closing segment in getTotalLength() and pathLength, so a draw-on animation behaves the same either way.

Point Order: Why Sequence is Strategy

Points are drawn in the exact sequence they appear in the string. Changing the order changes the shape's topology.

For a triangle, the order doesn't change the silhouette. Once there are 4 or more points, it does. Scramble the order and a clean square becomes a self-intersecting "hourglass" or "bow-tie":

<svg viewBox="0 0 220 125" width="440" height="250">
  <g>
    <polygon points="15,15 85,15 85,85 15,85" fill="#6366f1" fill-opacity="0.25" stroke="#4338ca" stroke-width="2" />
    <text x="8" y="12" font-size="8" text-anchor="middle" fill="currentColor">1</text>
    <text x="92" y="12" font-size="8" text-anchor="middle" fill="currentColor">2</text>
    <text x="92" y="96" font-size="8" text-anchor="middle" fill="currentColor">3</text>
    <text x="8" y="96" font-size="8" text-anchor="middle" fill="currentColor">4</text>
    <text x="50" y="118" font-size="8" text-anchor="middle" fill="currentColor">15,15 85,15 85,85 15,85</text>
  </g>
  <g transform="translate(120,0)">
    <polygon points="15,15 85,85 85,15 15,85" fill="#f43f5e" fill-opacity="0.25" stroke="#be123c" stroke-width="2" />
    <text x="8" y="12" font-size="8" text-anchor="middle" fill="currentColor">1</text>
    <text x="92" y="96" font-size="8" text-anchor="middle" fill="currentColor">2</text>
    <text x="92" y="12" font-size="8" text-anchor="middle" fill="currentColor">3</text>
    <text x="8" y="96" font-size="8" text-anchor="middle" fill="currentColor">4</text>
    <text x="50" y="118" font-size="8" text-anchor="middle" fill="currentColor">15,15 85,85 85,15 15,85</text>
  </g>
</svg>
1 2 3 4 15,15 85,15 85,85 15,85 1 2 3 4 15,15 85,85 85,15 15,85

Incorrect ordering can cause:

  • Self-intersections, where edges cross each other
  • Unexpected fills, because overlapping regions are now decided by the fill rule
  • Visual artifacts, such as spiky miter joins at the new, very sharp corners

When you generate points from data or geometry, sort them deliberately: by angle around a centre for radial shapes, or by x for time series.

Interactive Example

Try both behaviours yourself before moving on.

Free-Form Points has four independent point sliders that start as the square above. Drag them out of order into the self-intersecting hourglass. Then switch Element to polyline and watch the closing segment disappear. The demo sets fill="none" for the polyline, which is the fix for the phantom fill shown earlier.

Free-Form Points

0
20
100
0
20
100
0
80
100
0
20
100
0
80
100
0
80
100
0
20
100
0
80
100
1
2
6
<svg viewBox="0 0 100 100" width="240" height="240">
  <!-- Shape: the four draggable points, closed as a polygon or left open as a polyline. -->
  <polygon points="20,20 80,20 80,80 20,80" fill="#6366f1" fill-opacity="0.25" stroke="#4338ca" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />

  <!-- Guide points: the four vertices from the controls above. -->
  <circle cx="20" cy="20" r="4" fill="#4338ca" opacity="0.2" />
  <circle cx="20" cy="20" r="3" fill="#4338ca" />
  <circle cx="80" cy="20" r="4" fill="#4338ca" opacity="0.2" />
  <circle cx="80" cy="20" r="3" fill="#4338ca" />
  <circle cx="80" cy="80" r="4" fill="#4338ca" opacity="0.2" />
  <circle cx="80" cy="80" r="3" fill="#4338ca" />
  <circle cx="20" cy="80" r="4" fill="#4338ca" opacity="0.2" />
  <circle cx="20" cy="80" r="3" fill="#4338ca" />
</svg>

Shape Presets swaps the raw sliders for a Preset picker: square, hexagon or star. Switch Element between polygon and polyline on each to see the same open-vs-closed behaviour on a recognizable shape.

Shape Presets

1
2
6
<svg viewBox="0 0 100 100" width="240" height="240">
  <!-- Shape: the selected preset's vertices, closed as a polygon or left open as a polyline. -->
  <polygon points="25,25 75,25 75,75 25,75" fill="#6366f1" fill-opacity="0.25" stroke="#4338ca" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" />

  <!-- Guide points: every vertex of the current preset. -->
  <circle cx="25" cy="25" r="4" fill="#4338ca" opacity="0.2" />
  <circle cx="25" cy="25" r="3" fill="#4338ca" />
  <circle cx="75" cy="25" r="4" fill="#4338ca" opacity="0.2" />
  <circle cx="75" cy="25" r="3" fill="#4338ca" />
  <circle cx="75" cy="75" r="4" fill="#4338ca" opacity="0.2" />
  <circle cx="75" cy="75" r="3" fill="#4338ca" />
  <circle cx="25" cy="75" r="4" fill="#4338ca" opacity="0.2" />
  <circle cx="25" cy="75" r="3" fill="#4338ca" />
</svg>

Fill Rules: Managing Overlap

When a shape overlaps itself, like a five-pointed star or a figure-eight, SVG needs a rule to decide which regions are "inside" (filled) and which are "outside" (transparent). That rule is the fill-rule property, and both of its values use the same test:

  1. Pick a point you want to classify.
  2. Draw a ray from that point out to infinity, in any direction.
  3. Look at every edge of the shape that crosses the ray.

The two rules differ only in how they score those crossings.

nonzero (default)

nonzero cares about the direction each edge was drawn in, which comes from the order of your points. Start a counter at zero. Each time an edge crosses the ray from left to right (as seen looking along the ray), add 1. Each time one crosses from right to left, subtract 1. The final count is the point's winding number. If it's anything other than zero, the point is inside.

The sign of the result doesn't matter, only whether it's zero. Reverse every point and each +1 becomes −1, so a winding number of 2 becomes −2. That's still non-zero, and the fill doesn't change.

evenodd

evenodd ignores direction completely and just counts crossings. An odd count means inside and the point is filled. An even count means outside and it's left empty.

Walking Through the Star

Here's the same test applied to two points in a star drawn as a single <polygon>. Each small arrowhead shows the direction that edge was drawn in:

Ray-crossing test for nonzero and evenodd fill rulesA five-pointed star polygon with arrows showing the direction each edge is drawn. A ray from point A at the centre travels right and crosses two edges that both run the same way, counting +1 and +1 for a winding number of 2, so nonzero fills the centre; the same two crossings are an even count, so evenodd leaves it empty. A ray from point B inside the left arm crosses one edge, which is +1 under nonzero and odd under evenodd, so both rules fill the arm.+1+1+1ABA — ray from the centrecrosses 2 edges, both in the same directionnonzero: +1 +1 = 2 → filledevenodd: 2 crossings → even → emptyB — ray from inside an armcrosses 1 edgenonzero: +1 = 1 → filledevenodd: 1 crossing → odd → filledOnly the centre changes between the two rules.arrowheads: edge drawing order · dashed: test rays · star shown with nonzero (the default)
  • Point A, in the central pentagon: its ray crosses two edges, and both run the same way. Under nonzero that's +1 +1 = 2, so the centre is filled. Under evenodd it's 2 crossings, an even number, so the centre is empty.
  • Point B, in one of the arms: its ray crosses a single edge. That's 1 under nonzero and odd under evenodd, so both rules fill the arms.

That's the whole difference. The rules only disagree about regions whose winding number is even but not zero (2, −2, 4…). An arm is enclosed once, so both fill it; the centre pentagon is enclosed twice in the same direction, so only nonzero does.

Side-by-Side Comparison

The same points string rendered with each rule:

<svg viewBox="0 0 220 110" width="440" height="220">
  <g>
    <polygon points="50,5 76.4,86.4 7.2,36.1 92.8,36.1 23.5,86.4" fill="#8b5cf6" fill-opacity="0.6" stroke="#6d28d9" stroke-width="2" fill-rule="nonzero" />
    <text x="50" y="105" font-size="9" text-anchor="middle" fill="currentColor">fill-rule: nonzero</text>
  </g>
  <g transform="translate(120,0)">
    <polygon points="50,5 76.4,86.4 7.2,36.1 92.8,36.1 23.5,86.4" fill="#8b5cf6" fill-opacity="0.6" stroke="#6d28d9" stroke-width="2" fill-rule="evenodd" />
    <text x="50" y="105" font-size="9" text-anchor="middle" fill="currentColor">fill-rule: evenodd</text>
  </g>
</svg>
fill-rule: nonzero fill-rule: evenodd

The stroke is identical in both, because fill-rule only affects the fill.

What a Single <polygon> Can't Do: Holes

It's tempting to reach for evenodd to punch a hole in a shape, like a donut, a picture frame or the letter "O". A <polygon> can't do that. It's always exactly one closed outline (one subpath). With one outline, the only regions a fill rule can ever hollow out are the ones created where the outline crosses itself, as in the star.

A real hole needs a second outline, and that means <path> with multiple subpaths. You can build it in either of two ways:

  • evenodd: draw the inner outline in any direction. Its region is enclosed twice, which is an even count, so it's empty.
  • nonzero: draw the inner outline in the opposite direction to the outer one. Its region's winding number is +1 −1 = 0, so it's empty.
<svg viewBox="0 0 220 110" width="440" height="220">
  <g>
    <path d="M10,10 H90 V90 H10 Z M30,30 H70 V70 H30 Z" fill="#0ea5e9" fill-opacity="0.5" stroke="#0369a1" stroke-width="2" fill-rule="evenodd" />
    <text x="50" y="105" font-size="8" text-anchor="middle" fill="currentColor">evenodd, same direction</text>
  </g>
  <g transform="translate(120,0)">
    <path d="M10,10 H90 V90 H10 Z M30,30 V70 H70 V30 Z" fill="#0ea5e9" fill-opacity="0.5" stroke="#0369a1" stroke-width="2" fill-rule="nonzero" />
    <text x="50" y="105" font-size="8" text-anchor="middle" fill="currentColor">nonzero, inner reversed</text>
  </g>
</svg>
evenodd, same direction nonzero, inner reversed

Why This Matters

  • Stars and overlapping icons: decide up front whether the doubly-enclosed centre should be solid (nonzero) or knocked out (evenodd), instead of leaving it to the default.
  • Geometric patterns: with evenodd, regions where a self-intersecting outline overlaps itself alternate between filled and empty.
  • Data viz: an area polygon whose data line crosses back over itself can create doubly-covered regions. Check which rule gives the result you intend.

Summary: nonzero fills every region the outline winds around, so self-overlaps stay solid. evenodd drops the regions covered an even number of times and keeps everything else. It doesn't turn a shape into a wireframe; it only changes the overlaps.

Corners and the Miter Limit

Polygons and polylines are made entirely of straight segments, so their corners (the joins) and their loose ends (the caps) have an outsized effect on how polished they look. The basics are covered in the basic shapes post. In short:

  • stroke-linejoin shapes every corner: miter (the default) gives sharp points, round gives rounded ones and bevel cuts them off flat.
  • stroke-linecap shapes loose ends: butt (the default), round or square. On a polygon, it only matters when the stroke is dashed, since each dash then gets its own caps.

The attribute that surprises people is the third one.

stroke-miterlimit in Depth

A miter join extends the two outer edges of the stroke until they meet. The sharper the corner, the farther away they meet. At a nearly-closed corner the spike would shoot out an absurd distance, so the renderer measures it first:

  • The miter length is the distance from the inner corner of the join to the outer tip.
  • For a corner of angle θ, the ratio miter length ÷ stroke-width = 1 / sin(θ / 2).
  • stroke-miterlimit sets the maximum allowed ratio. It defaults to 4, and values below 1 are invalid.
  • If a corner's ratio is over the limit, that one join is drawn as a bevel instead. The other corners keep their miters.

With the default of 4, the cutoff is at θ = 2 · asin(1/4) ≈ 29°. Any corner sharper than about 29° gets bevelled:

How stroke-miterlimit decides between a miter and a bevelTwo thick strokes, each bending through a sharp corner. On the left a 30 degree corner has a miter length of about 3.86 times the stroke width, which is within the default miter limit of 4, so the pointed miter is drawn. On the right a 20 degree corner would need a miter 5.76 times the stroke width; that exceeds 4, so the renderer cuts the corner off flat with a bevel, and the missing spike is outlined with a dashed line.30°30° corner1 / sin(15°) ≈ 3.86 ≤ 4→ miter is drawn20°20° corner1 / sin(10°) ≈ 5.76 > 4→ falls back to bevelmiter length≈ 3.86 × ww = 24miter needed: 5.76 × wbevelratio = miter length ÷ stroke-width = 1 / sin(θ ÷ 2)stroke-miterlimit defaults to 4, so any corner sharper than ≈ 29° is bevelled

That's why a star can come out looking "blunt" for no obvious reason. The regular five-pointed star from earlier has 36° tips (ratio ≈ 3.24), so its miters survive. Make the arms a little skinnier and the tips drop below 29°:

<svg viewBox="0 0 220 110" width="440" height="220">
  <g>
    <polygon points="50,8 57.1,40.3 89.9,37 61.4,53.7 74.7,84 50,62 25.3,84 38.6,53.7 10.1,37 42.9,40.3" fill="#f59e0b" fill-opacity="0.25" stroke="#b45309" stroke-width="5" stroke-linejoin="miter" />
    <text x="50" y="104" font-size="8" text-anchor="middle" fill="currentColor">miterlimit 4 (default)</text>
  </g>
  <g transform="translate(120,0)">
    <polygon points="50,8 57.1,40.3 89.9,37 61.4,53.7 74.7,84 50,62 25.3,84 38.6,53.7 10.1,37 42.9,40.3" fill="#f59e0b" fill-opacity="0.25" stroke="#b45309" stroke-width="5" stroke-linejoin="miter" stroke-miterlimit="5" />
    <text x="50" y="104" font-size="8" text-anchor="middle" fill="currentColor">miterlimit 5</text>
  </g>
</svg>
miterlimit 4 (default) miterlimit 5

Each tip of this star is about 24.6°, which gives a ratio of about 4.69. With the default limit of 4 every tip is bevelled. Raising the limit to 5 lets the spikes through. The inner, concave corners are wide, so they're never affected.

Try it below. The corner starts at 25°, already past the limit, so the readout says bevel. Widen it past 29° to get the point back, then narrow it again and raise stroke-miterlimit instead. Stroke width changes how big the spike is but not the ratio, which depends only on the angle:

Miter Limit

5
25
170
1
4
8
2
8
10
<svg viewBox="0 0 120 120" width="240" height="240">
  <!-- Readout: miter length ÷ stroke-width = 1 / sin(angle / 2) = 4.62 -->
  <text x="60" y="10" font-size="7" text-anchor="middle" fill="currentColor">ratio 4.62 > 4 → bevel</text>

  <!-- Corner: a two-segment polyline bending at (60, 62). -->
  <polyline points="49.2,110.8 60,62 70.8,110.8" fill="none" stroke="#0ea5e9" stroke-width="8" stroke-linejoin="miter" stroke-miterlimit="4" />
</svg>
ratio 4.62 > 4 → bevel

Generating Points with JavaScript

Hand-writing coordinates works for a triangle. For anything regular, or anything data-driven, generate them.

Regular Polygons and Stars

Every vertex of a regular polygon sits on a circle, so it's just cos and sin at evenly spaced angles. A star is the same idea with twice as many vertices, alternating between an outer and an inner radius:

// Returns a points string for a regular polygon (innerRadius omitted)
// or a star (innerRadius given) centred on cx, cy.
function polygonPoints(cx, cy, sides, radius, innerRadius) {
  const count = innerRadius ? sides * 2 : sides;
  const start = -Math.PI / 2; // first vertex at 12 o'clock (SVG's y-axis points down)

  return Array.from({ length: count }, (_, i) => {
    const r = innerRadius && i % 2 === 1 ? innerRadius : radius;
    const angle = start + (i * 2 * Math.PI) / count;
    const x = cx + r * Math.cos(angle);
    const y = cy + r * Math.sin(angle);
    return x.toFixed(1) + "," + y.toFixed(1);
  }).join(" ");
}

polygonPoints(50, 50, 6, 40);      // hexagon
polygonPoints(50, 50, 5, 42, 18);  // five-pointed star

Two details are worth copying:

  • Start at −90°. In SVG, angle 0 points right and angles increase clockwise, because y grows downward. Starting at −π/2 puts the first vertex at the top, so the shape stands upright.
  • Round with toFixed(). Floating-point output like 84.64101615137754 makes the markup longer without looking any different. One or two decimal places is plenty for a typical viewBox.

Note that this star is drawn as its outline (10 vertices around the edge), not as a self-intersecting pentagram. It has no doubly-covered centre, so it looks the same under either fill rule.

The playground below runs the same generator. Points / sides sets how many outer vertices there are. Shape switches between a regular polygon and a star, and Inner radius sets how deep the star's notches go (it has no effect in regular mode). Rotation turns the whole shape around its centre. The code view shows the exact points string the function produces:

Generated Points

3
5
12
0.2
0.45
0.9
0
0
359
<svg viewBox="0 0 100 100" width="240" height="240">
  <!-- 10 generated vertices around (50, 50), rounded to one decimal place. -->
  <polygon points="50.0,8.0 61.1,34.7 89.9,37.0 68.0,55.8 74.7,84.0 50.0,68.9 25.3,84.0 32.0,55.8 10.1,37.0 38.9,34.7" fill="#6366f1" fill-opacity="0.25" stroke="#4338ca" stroke-width="2" stroke-linejoin="round" />
</svg>

Mapping Data to Points

For a chart, each value needs to be mapped from data space into the viewBox. That takes a linear scale on each axis, and the y-axis needs flipping because SVG's y grows downward:

// Maps an array of numbers onto a width × height viewBox, leaving pad units of breathing room.
function toPoints(data, width, height, pad = 2) {
  const min = Math.min(...data);
  const max = Math.max(...data);
  const xStep = (width - pad * 2) / (data.length - 1);
  const yScale = (height - pad * 2) / (max - min || 1); // || 1 guards a flat series

  return data
    .map((value, i) => {
      const x = pad + i * xStep;
      const y = height - pad - (value - min) * yScale; // flip: larger values sit higher
      return x.toFixed(1) + "," + y.toFixed(1);
    })
    .join(" ");
}

document.querySelector("#trend").setAttribute("points", toPoints([12, 15, 11, 19, 17, 24, 22, 27], 100, 30));

Charting libraries like D3 wrap exactly this in scaleLinear(), but for a single line, a dozen lines of code is all you need.

The Live points List: SVGPointList

You don't have to rebuild the whole string to change one vertex. Every <polyline> and <polygon> exposes its vertices as a live points list, and changing an item redraws the shape:

const line = document.querySelector("#trend");

line.points.numberOfItems;          // how many vertices
const first = line.points.getItem(0);
first.y = 5;                        // moves the first vertex, redraws immediately

// Append a new vertex
const svg = line.ownerSVGElement;
const point = svg.createSVGPoint();
point.x = 98;
point.y = 4;
line.points.appendItem(point);

insertItemBefore(), replaceItem(), removeItem() and clear() round out the list API. createSVGPoint() is the most widely supported way to make a point object. SVG 2 lets list items be plain DOMPoints, but test that in your target browsers before depending on it. When the whole dataset changes, setting the points attribute once is simpler, and usually cheaper, than dozens of individual list mutations.

Sparklines: Tiny Charts That Stretch

A sparkline is a word-sized line chart that sits inline with text or in a table cell, with no axes and no labels. It's the most natural job there is for <polyline>, and it shows off three attributes working together:

<svg viewBox="0 0 100 30" width="240" height="48" preserveAspectRatio="none" role="img" aria-labelledby="sparkline-title">
  <title id="sparkline-title">Weekly sign-ups, rising from 12 to 27 over eight weeks</title>
  <polyline points="2,26.4 15.7,21.5 29.4,28 43.1,15 56.9,18.3 70.6,6.9 84.3,10.1 98,2" fill="none" stroke="#6366f1" stroke-width="2" stroke-linejoin="round" stroke-linecap="round" vector-effect="non-scaling-stroke" />
  <line x1="98" y1="2" x2="98" y2="2" stroke="#6366f1" stroke-width="6" stroke-linecap="round" vector-effect="non-scaling-stroke" />
</svg>
Weekly sign-ups, rising from 12 to 27 over eight weeks
  • viewBox="0 0 100 30" gives the data a fixed coordinate space. These points came straight from the toPoints() function above.
  • preserveAspectRatio="none" lets that space stretch to whatever box the sparkline is given, whether a narrow table cell or a full-width card, without letterboxing. The line is squashed or stretched, which is fine, because a sparkline shows shape, not exact values.
  • vector-effect="non-scaling-stroke" fixes the side effect. Without it, a non-uniform stretch distorts the stroke too, so the line gets thick on steep sections and thin on flat ones. With it, the stroke width is applied in screen pixels after the stretch, so the line is a consistent 2px everywhere. This is a visual fix, not a performance one.

The last-point dot uses a small trick: it's a zero-length <line> with a round cap, which renders as a dot the width of the stroke. A <circle> would be squashed into an ellipse by the non-uniform stretch, but the cap is also non-scaling-stroke, so it stays round. If you'd rather not rely on that, position an HTML dot over the SVG instead.

Markers on Vertices

Charts often need a dot on every data point or an arrowhead at the end of a route. You don't need to compute those positions: <polyline> and <polygon> both support marker-start, marker-mid and marker-end, and the browser places a marker on each vertex for you:

<svg viewBox="0 0 120 70" width="360" height="210">
  <defs>
    <marker id="pvp-marker-dot" viewBox="0 0 10 10" refX="5" refY="5" markerWidth="4" markerHeight="4">
      <circle cx="5" cy="5" r="4" fill="#6366f1" />
    </marker>
    <marker id="pvp-marker-arrow" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="5" markerHeight="5" orient="auto">
      <path d="M0,0 L10,5 L0,10 Z" fill="#6366f1" />
    </marker>
  </defs>
  <polyline points="10,55 35,30 60,40 85,15 105,20" fill="none" stroke="#6366f1" stroke-width="2" stroke-linejoin="round" marker-start="url(#pvp-marker-dot)" marker-mid="url(#pvp-marker-dot)" marker-end="url(#pvp-marker-arrow)" />
</svg>

marker-mid lands on every interior vertex, so adding a data point to the points list adds its dot automatically.

Draw-On Animation

The "line drawing itself" effect works on <polyline> and <polygon> exactly as it does on <path>. Set pathLength="1" so the shape's length is normalised to 1. Then use a dash as long as the whole shape, and slide it into view with stroke-dashoffset:

<svg viewBox="0 0 100 60">
  <style>
    .pvp-draw {
      stroke-dasharray: 1;
      stroke-dashoffset: 1;
      animation: pvp-draw 2s ease-out forwards;
    }

    @keyframes pvp-draw {
      to { stroke-dashoffset: 0; }
    }

    @media (prefers-reduced-motion: reduce) {
      .pvp-draw { animation: none; stroke-dashoffset: 0; }
    }
  </style>

  <polyline class="pvp-draw" pathLength="1" points="5,50 25,20 45,35 65,10 95,25" fill="none" stroke="#10b981" stroke-width="3" stroke-linejoin="round" />
</svg>

A few things to keep in mind:

  • On a polygon, the closing segment is part of the length, so the outline draws all the way back to its start.
  • pathLength is defined for all basic shapes in SVG 2. If an engine you support ignores it on a polyline, measure the real length with element.getTotalLength() and use that number for the dasharray and offset instead of 1.
  • A shape that's still filled will show its full fill from the first frame. Draw-on is a stroke effect, so either use fill="none" or fade the fill in separately.

The path element post walks through the dash maths step by step, and Animating SVG: A Guide to CSS Transitions and Keyframes covers triggering it on hover or scroll.

Morphing Shapes: Animating points

Unlike <path>'s d, which some browsers expose as a CSS property, points is not a CSS property. You can't put it in a transition or @keyframes rule. You have three options:

  • SMIL <animate attributeName="points">, declared right inside the shape.
  • JavaScript: interpolate each coordinate yourself in a requestAnimationFrame loop and write the result back.
  • An animation library (GSAP, anime.js and similar), which handles the interpolation for you.

All three interpolate point by point, so both shapes need the same number of points. If the counts differ, the browser can't pair the vertices up, and SMIL falls back to a discrete jump from one shape to the other with no tween. To morph a triangle into a square, pad the triangle to four points by repeating its top vertex, so the apex splits into the square's two top corners:

<svg viewBox="0 0 100 100">
  <polygon points="50,10 50,10 90,90 10,90" fill="#f59e0b" fill-opacity="0.5" stroke="#b45309" stroke-width="2">
    <animate
      attributeName="points"
      dur="2s"
      repeatCount="indefinite"
      values="50,10 50,10 90,90 10,90;
              10,10 90,10 90,90 10,90;
              50,10 50,10 90,90 10,90" />
  </polygon>
</svg>

Where you put the duplicate matters. Each vertex travels in a straight line to its partner, so match vertices that are close together, or the shape will twist through itself mid-morph.

Putting it into Practice

These examples show how to choose the right element for common jobs.

Basic: Geometric Essentials

1. The Simplest Triangle (<polygon>)

The most fundamental polygon is a triangle. With just three points, SVG connects the last vertex back to the first and encloses the shape.

<svg viewBox="0 0 100 100" width="220" height="220">
  <polygon points="50,10 90,85 10,85" fill="#38bdf8" fill-opacity="0.25" stroke="#0369a1" stroke-width="3" stroke-linejoin="round" />
</svg>

Why it works: stroke-linejoin="round" softens the corners, which gives the shape a more modern, friendly look. It also sidesteps the miter limit entirely.

2. Route Map Visualization (<polyline>)

A route shouldn't close back on itself, which makes it a natural polyline. Here it's drawn as a dashed "connect-the-dots" path.

<svg viewBox="0 0 100 100" width="240" height="240">
  <polyline points="10,85 30,55 50,70 70,25 90,15" fill="none" stroke="#10b981" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="6 4" />
  <circle cx="10" cy="85" r="3" fill="#10b981" />
  <circle cx="90" cy="15" r="3" fill="#10b981" />
</svg>

Pro Tip: fill="none" avoids the phantom fill, and stroke-dasharray gives the dashed look common in map apps. The start and end circles could just as well be marker-start and marker-end.

Advanced: Data & UI

1. The Star Shape (evenodd Rule)

When a polygon crosses itself, as this pentagram does, fill-rule decides whether the doubly-enclosed centre is hollow or solid.

<svg viewBox="0 0 100 100" width="240" height="240">
  <polygon points="50,5 76.4,86.4 7.2,36.1 92.8,36.1 23.5,86.4" fill="#8b5cf6" fill-opacity="0.6" stroke="#6d28d9" stroke-width="2" fill-rule="evenodd" />
</svg>

Why it matters: evenodd knocks out the self-overlapping centre with a single element. It works because the outline crosses itself. It can't add a separate hole to a shape that doesn't overlap itself; for that, see holes.

2. The Hybrid Area Chart

Data visualization often combines both elements. A polyline draws the trend line, and a polygon shades the area underneath. Here the area fades out using a vertical gradient:

<svg viewBox="0 0 100 100" width="280" height="180">
  <defs>
    <linearGradient id="pvp-area-fade" x1="0" y1="0" x2="0" y2="1">
      <stop offset="0" stop-color="#6366f1" stop-opacity="0.35" />
      <stop offset="1" stop-color="#6366f1" stop-opacity="0" />
    </linearGradient>
  </defs>
  <polygon points="0,100 0,70 20,50 40,60 60,30 80,45 100,20 100,100" fill="url(#pvp-area-fade)" stroke="none" />
  <polyline points="0,70 20,50 40,60 60,30 80,45 100,20" fill="none" stroke="#6366f1" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

Strategy: For the polygon to sit flat against the bottom, include the "floor" coordinates (0,100 and 100,100) in its points. Keep the stroke on a separate polyline. Otherwise, the polygon would also stroke its two vertical sides and the floor.

3. Geometric UI Hexagon

Polygons are the go-to for rigid UI elements like hex-grid buttons or achievement badges.

<svg viewBox="0 0 100 100" width="220" height="220">
  <polygon points="50,10 84.6,30 84.6,70 50,90 15.4,70 15.4,30" fill="#f43f5e" fill-opacity="0.2" stroke="#be123c" stroke-width="3" stroke-linejoin="round" />
</svg>

Context: A handful of bytes of markup replaces an image file, and the shape stays crisp at any pixel density or zoom level. Set Preset to hexagon in the Shape Presets playground above to try it, or generate one with polygonPoints(50, 50, 6, 40).

When to Switch to <path>

<polyline> and <polygon> are shorthands for one specific kind of <path>: a single subpath of straight lines. Reach for <path> as soon as you need any of these:

  • Curves: Bézier curves (C, S, Q, T), for smooth chart lines, organic shapes or rounded corners on only some vertices.
  • Arcs (A), for pie slices, gauges and rounded ends of a specific radius.
  • Holes or multiple outlines: several subpaths in one element, as in the square frame above.
  • Many disjoint lines in one element: grid lines, tick marks or a set of separate routes as one DOM node, each starting with its own M.

Converting is mechanical. A polyline's points become an M followed by L commands, and a polygon adds a closing Z:

<polyline points="10,80 40,20 70,60" />
<path d="M10,80 L40,20 L70,60" />

<polygon points="10,80 40,20 70,60" />
<path d="M10,80 L40,20 L70,60 Z" />

The Z is what gives you the polygon's clean closing join. Ending a path with L10,80 instead would recreate the repeated-point seam. Going the other way, if a path is a single M followed only by L, H, V and at most one closing Z, a polyline or polygon is usually easier to read and to generate.

Performance: Point Density

A few dozen or a few hundred points cost nothing. Trouble starts with thousands of points, such as a high-resolution map outline or an "all-time" stock chart, especially when they're updated often. What actually helps:

  • Draw fewer points. A 600px-wide chart can't show more than a point or two per pixel column, so 10,000 raw samples are mostly invisible. Simplify the line first with the Ramer–Douglas–Peucker algorithm (for example, the simplify.js library), or downsample to one min/max pair per pixel. This is by far the biggest win.
  • Round your coordinates. toFixed(1) or toFixed(2) shrinks the markup without any visible change.
  • Transform instead of recomputing. To pan or zoom a chart, put a transform on a parent <g> or change the viewBox, rather than rewriting every coordinate on every frame.
  • Don't reset points every frame unless the data really changed. Batch updates, and for a single moving vertex, use the live points list.
  • Know when to leave SVG. For tens of thousands of points, or for real-time streams redrawn many times a second, <canvas> is usually the better tool, because it paints pixels without keeping a DOM node per shape.

vector-effect="non-scaling-stroke" often shows up in performance advice, but it's a visual fix that keeps stroke width consistent under scaling. See Sparklines for where it belongs.

Accessibility Considerations

<polygon> and <polyline> are ordinary DOM elements, so the general guidance from SVG Essentials: Mastering Shapes, Coordinates, and Styling applies. Two patterns come up often enough with point-based shapes to call out on their own.

Icons and badges built from <polygon>: if the shape conveys meaning on its own (a hexagon "verified" badge, a star rating), give it a <title> and role="img". If it's purely decorative and sits next to a text label that already says the same thing, hide it with aria-hidden="true".

Charts built from <polyline>: a points string is just coordinates, so there's nothing in it a screen reader can announce. Give the chart a short name and a one-sentence summary of the trend, and make the exact values available as a real table:

<figure>
  <svg viewBox="0 0 100 30" role="img" aria-labelledby="signups-title signups-desc">
    <title id="signups-title">Weekly sign-ups, weeks 1 to 8</title>
    <desc id="signups-desc">Sign-ups more than doubled, from 12 in week 1 to 27 in week 8, with small dips in weeks 3, 5 and 7.</desc>
    <polyline points="2,26.4 15.7,21.5 29.4,28 43.1,15 56.9,18.3 70.6,6.9 84.3,10.1 98,2"
              fill="none" stroke="currentColor" stroke-width="2" />
  </svg>

  <details>
    <summary>Show the data</summary>
    <table>
      <thead><tr><th scope="col">Week</th><th scope="col">Sign-ups</th></tr></thead>
      <tbody>
        <tr><td>1</td><td>12</td></tr>
        <tr><td>2</td><td>15</td></tr>
        <!-- … weeks 3–8 … -->
      </tbody>
    </table>
  </details>
</figure>
  • role="img" plus aria-labelledby pointing at both the <title> and <desc> gives the graphic a name and a description. It also tells assistive tech to treat the SVG as one image, instead of walking its children.
  • The <desc> states the takeaway, not the coordinates. That's what a sighted reader gets from a glance.
  • The table carries the exact values for anyone who needs them, and it's useful to sighted users too. A visually-hidden table works as well, if you don't want a disclosure widget.

Best Practices

  1. Always define fill for polylines. The default is fill="black", so set fill="none" explicitly to avoid accidental shading.
  2. Close with <polygon>, not a repeated point. A repeated first point leaves a capped seam instead of a join.
  3. Order points deliberately. They're connected exactly in the order written, so sort generated points by angle or by x.
  4. Watch sharp corners. Corners under about 29° are bevelled by the default stroke-miterlimit="4". Raise the limit on purpose, or use stroke-linejoin="round" for data-driven shapes.
  5. Choose your fill-rule deliberately. For self-intersecting shapes, decide up front between a solid (nonzero) and a knocked-out (evenodd) centre. For real holes, switch to <path>.
  6. Round generated coordinates with toFixed(), and simplify dense data before rendering it.
  7. Use a consistent coordinate grid. A predictable grid like viewBox="0 0 100 100" makes positioning and generating points easier.
  8. Give meaningful shapes a name. Icons and badges need a <title> (or aria-hidden="true" if decorative), and charts need a summary plus the data.

Where Should You Use This?

Use <polyline> when:

  • Building line charts and sparklines
  • Drawing routes, connectors or signal traces
  • Connecting data points, with markers on the vertices
  • Animating a line drawing itself in

Use <polygon> when:

  • Creating geometric shapes, stars, hexagons and badges
  • Filling closed areas, like the area under a chart or map regions
  • Designing rigid UI illustrations
  • Morphing between shapes with matching point counts

Use <path> when you need curves, arcs, holes or several outlines in one element. See When to Switch to <path>.

Frequently Asked Questions

Can a <polyline> be filled?

Yes, and by default it will be, whether you want it to or not. SVG's initial fill value is black, and a polyline is no exception, even though it's an "open" shape. The fill covers the region enclosed by the line plus an imaginary chord from the last point back to the first. Add fill="none" unless you want that region shaded.


Does <polygon> support stroke-dasharray?

Yes. A dashed polygon still closes itself, and the dash pattern runs continuously around the whole outline, including the auto-generated closing segment. Each dash gets its own stroke-linecap.


Can I animate the points attribute?

Yes, but not with CSS. points isn't a CSS property. Use SMIL's <animate attributeName="points">, JavaScript or an animation library. Smooth interpolation only happens between lists with the same number of points. Mismatched counts jump from one shape to the other instead. See Morphing Shapes.


Why does fill-rule change the filled area but not the outline?

Because fill-rule only governs the fill. It decides which enclosed regions count as "inside". The stroke follows the same edges whichever rule is active, so the outline never changes; only the shaded area does.


Why are the tips of my star blunt?

The tips are sharper than about 29°, so they exceed the default stroke-miterlimit of 4 and fall back to bevels. Raise stroke-miterlimit, or accept rounded tips with stroke-linejoin="round". See the miter limit.


Can a <polygon> or <polyline> have curved segments?

No. Both elements only connect their points with straight lines. For Bézier curves and arcs, use <path>.

Wrapping Up

<polygon> and <polyline> share the exact same points syntax, and most of the time the choice comes down to one question: does this shape need to close?

  • Use polyline for data visualizations, sparklines, route maps and anything that should stay open. Remember fill="none".
  • Use polygon for icons, badges and geometric shapes that need a sealed, cleanly-joined outline. Don't fake one by repeating the first point.
  • Watch your fill-rule whenever an outline crosses itself. nonzero counts direction and evenodd counts crossings, and they only disagree about regions wound an even, non-zero number of times.
  • Mind the miter limit on sharp corners: past about 29°, the default of 4 turns spikes into bevels.
  • Generate, don't hand-type: a little cos/sin or a linear scale produces precise, rounded points for stars, polygons and charts.
  • Graduate to <path> for curves, arcs and holes.

Both elements trace back to the six basic shapes covered in SVG Essentials: Mastering Shapes, Coordinates, and Styling, which is worth a revisit if the points syntax or coordinate system ever feels shaky. From here, SVG Essentials: Mastering the Path Element picks up where straight lines leave off.

From the point-scrambling playground that turns a clean square into a self-intersecting hourglass mid-drag and the square, hexagon, and star presets, to the ray diagram that finally shows why a star's centre winds twice, the miter-limit slider that snaps a spike into a bevel right at 29°, and the generated points behind a sparkline that stretches without its stroke going wonky, this post leaned on live demos to do work that formulas alone never quite pull off. Thanks for dragging every vertex, flipping every fill rule, and sharpening every corner with me — go close off a shape that's entirely your own. ⬡

  • SVG Essentials: Mastering the Path Element

    Understand SVG paths, curves, and commands
    A complete guide to the SVG path element — path commands, coordinates, bézier curves, arcs, compound paths and fill-rule, pathLength-based line-draw animation, CSS's path() function, stroke rendering, and the shorthand tricks behind exported path data.
    Published at:
  • SVG Essentials: Mastering Shapes, Coordinates, and Styling

    Build sharp, scalable graphics with basic shapes.
    Learn how to build resolution-independent SVG graphics. Master the viewBox, coordinates, essential shapes, and CSS styling with fill, stroke, and hover animations.
    Published at:
  • HTML5 Canvas Guide: Drawing, Animation, and Best Practices

    Draw, animate, and optimise graphics with the Canvas API.
    Learn the HTML5 Canvas API from first shapes to advanced techniques — paths, transforms, animation, input, high-DPI, OffscreenCanvas, pixels, and 3D.
    Published at: