SVG markers: arrowheads on lines and paths

Draw an arrowhead once inside a marker element and SVG places it on the ends and corners of any line, turned to match the direction. This page covers each attribute, with live examples you can edit.

An SVG marker is a small drawing, usually an arrowhead, that the browser places on the points of a line. Draw it once inside a <marker> element and attach it with marker-end, marker-start or marker-mid. With orient="auto", the head follows the line.

Try it first. Click or drag in the box and the curve bends to the pointer, with its head turning as it goes.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SVG arrow with a marker</title>
<style>
  body { margin: 0; font-family: system-ui, sans-serif; background: #f4f5f7; }
  p { margin: 10px 14px 0; font-size: 14px; color: #444; }
  svg {
    display: block; width: 100%; height: 270px;
    touch-action: none;  /* a finger moves the arrow instead of scrolling */
    cursor: crosshair;
  }
</style>
</head>
<body>
<p>Click or drag anywhere. The arrowhead turns to follow the line.</p>
<svg id="svg">
  <defs>
    <!-- one arrowhead, drawn once in a 10 x 10 box -->
    <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5"
            markerWidth="6" markerHeight="6" orient="auto">
      <path d="M0 0 L10 5 L0 10 z" fill="#2563eb"/>
    </marker>
  </defs>
  <circle cx="40" cy="200" r="5" fill="#2563eb"/>
  <path id="line" d="M40 200 Q 140 40 260 120" fill="none"
        stroke="#2563eb" stroke-width="3" marker-end="url(#arrow)"/>
</svg>

<script>
  const svg = document.getElementById('svg');
  const line = document.getElementById('line');

  function aim(e) {
    const r = svg.getBoundingClientRect();
    const x = e.clientX - r.left, y = e.clientY - r.top;
    // a curve from the start point to the pointer; the marker sits on its end
    line.setAttribute('d', `M40 200 Q ${(40 + x) / 2} 40 ${x} ${y}`);
  }

  svg.addEventListener('pointerdown', (e) => {
    svg.setPointerCapture(e.pointerId);
    aim(e);
  });
  svg.addEventListener('pointermove', (e) => {
    if (svg.hasPointerCapture(e.pointerId)) aim(e);
  });
</script>
</body>
</html>
One marker on the end of a curve. Edit the code and the example reruns.

The arrowhead is defined once, and every line in the page can reuse it.

The marker element, piece by piece

A marker has its own small drawing space. The arrowhead is drawn in that space, then scaled and moved so that one chosen point lands on the line.

<defs>
  <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5"
          markerWidth="6" markerHeight="6" orient="auto">
    <path d="M0 0 L10 5 L0 10 z" fill="#2563eb"/>
  </marker>
</defs>
<path d="M40 200 Q 140 40 260 120" fill="none" stroke="#2563eb"
      stroke-width="3" marker-end="url(#arrow)"/>
The head is drawn in a 10 by 10 box. The ref point is the spot that sits on the end of the line.
The head is drawn in a 10 by 10 box. The ref point is the spot that sits on the end of the line.
Attribute What it sets In the example
viewBox The marker's own drawing space A 10 by 10 box
refX, refY The point placed on the line The tip, at 10,5
markerWidth, markerHeight How big the box is drawn (default 3) 6 stroke widths
orient How the box is turned (default 0) Follow the line
markerUnits What the width and height are measured in Stroke widths (default)

Put the tip on the ref point. With refX="0", the base of the triangle sits on the end point and the tip overshoots it by the length of the head.

marker-start, marker-mid and marker-end

Three properties decide where the marker goes. The value is always url(#id), pointing at the marker's id.

  • marker-start draws it on the first point.
  • marker-end draws it on the last point.
  • marker-mid draws it on every point in between. On a polyline, that is each corner.

Markers only appear on four elements: <path>, <line>, <polyline> and <polygon>. A <rect> or <circle> ignores them, so draw the shape as a path when it needs arrows.

The three can also be set in CSS, and the marker shorthand sets all three at once:

.edge { marker-end: url(#arrow); }
.route { marker: url(#dot); }  /* start, every corner, end */

orient: auto, auto-start-reverse or an angle

By default a marker is not turned at all (orient="0"). An arrow on a line that runs down or left would still point right. orient="auto" turns it to the direction of the path at that point.

The catch comes with a double-headed arrow. auto turns every copy to the direction the path travels, so the head at the start points inward, along the line, instead of out.

With auto, the start head points into the line. auto-start-reverse flips only the start copy.
With auto, the start head points into the line. auto-start-reverse flips only the start copy.

orient="auto-start-reverse" fixes it. The copy drawn by marker-start turns 180 degrees, and the others behave like auto. One marker then serves both ends. A number such as orient="45" fixes the angle in degrees, whatever the line does.

Switch between the options below, and move the stroke-width slider to see the next section at work.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>orient and markerUnits</title>
<style>
  body { margin: 0; padding: 12px 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #222; }
  .controls { display: flex; flex-wrap: wrap; gap: 10px 18px; font-size: 14px; }
  label { display: flex; align-items: center; gap: 6px; }
  select, input { font: inherit; }
  svg { display: block; width: 100%; height: 190px; margin-top: 10px; background: #fff; border-radius: 10px; }
  pre { margin: 10px 0 0; padding: 10px; font-size: 12.5px; background: #1f2430; color: #e6e8ee; border-radius: 8px; white-space: pre-wrap; }
</style>
</head>
<body>
<div class="controls">
  <label>orient
    <select id="orient">
      <option>auto</option>
      <option selected>auto-start-reverse</option>
      <option>0</option>
    </select>
  </label>
  <label>markerUnits
    <select id="units">
      <option selected>strokeWidth</option>
      <option>userSpaceOnUse</option>
    </select>
  </label>
  <label>stroke-width <input id="width" type="range" min="1" max="8" value="2"> <span id="wv">2</span></label>
</div>

<svg viewBox="0 0 400 190">
  <defs>
    <marker id="head" viewBox="0 0 10 10" refX="10" refY="5"
            markerWidth="5" markerHeight="5" orient="auto-start-reverse">
      <path d="M0 0 L10 5 L0 10 z" fill="#c2410c"/>
    </marker>
  </defs>
  <!-- the same marker on both ends of both shapes -->
  <line id="a" x1="40" y1="50" x2="360" y2="50" stroke="#c2410c" stroke-width="2"
        marker-start="url(#head)" marker-end="url(#head)"/>
  <path id="b" d="M40 150 C 140 80 260 200 360 120" fill="none" stroke="#c2410c" stroke-width="2"
        marker-start="url(#head)" marker-end="url(#head)"/>
</svg>

<pre id="code"></pre>

<script>
  const head = document.getElementById('head');
  const shapes = [document.getElementById('a'), document.getElementById('b')];
  const orient = document.getElementById('orient');
  const units = document.getElementById('units');
  const width = document.getElementById('width');

  function update() {
    head.setAttribute('orient', orient.value);
    head.setAttribute('markerUnits', units.value);
    // in user units, 5 x 5 would be tiny, so give a size in viewBox units
    const size = units.value === 'userSpaceOnUse' ? 14 : 5;
    head.setAttribute('markerWidth', size);
    head.setAttribute('markerHeight', size);
    shapes.forEach((s) => s.setAttribute('stroke-width', width.value));
    document.getElementById('wv').textContent = width.value;
    document.getElementById('code').textContent =
      `<marker id="head" viewBox="0 0 10 10" refX="10" refY="5"\n` +
      `        markerWidth="${size}" markerHeight="${size}"\n` +
      `        orient="${orient.value}" markerUnits="${units.value}">`;
  }

  [orient, units, width].forEach((el) => el.addEventListener('input', update));
  update();
</script>
</body>
</html>
The same marker on both ends of a line and a curve. Change orient, markerUnits and stroke-width.

markerUnits: should the head grow with the line?

markerWidth and markerHeight are measured in stroke widths by default (markerUnits="strokeWidth"). A head of markerWidth="4" on a 2 unit line is 8 units wide. On an 8 unit line it is 32. The arrow keeps its shape relative to the line.

Left: the head scales with stroke-width. Right: it stays one size.
Left: the head scales with stroke-width. Right: it stays one size.

markerUnits="userSpaceOnUse" measures them in the SVG's own units instead. The head stays one size, whatever the stroke. That suits diagrams where lines vary in thickness but arrows should match. Use a larger number, since 4 units is small.

One trade-off: a thick line can then cover a small head.

One marker for every colour: context-stroke

The shapes inside a marker do not inherit from the line that uses it. They inherit from the marker's own ancestors. A blue line with a marker drawn in black gets a black head. The traditional fix is one marker per colour.

SVG 2 added the value context-stroke for fill and stroke. Inside a marker, it means "the stroke of the element this marker is on". We measured it in the three browser engines that Playwright ships, painting a red line with a fill="context-stroke" head:

Engine Head colour Fallback colour kept
Chromium Red, same as the line Not needed
Firefox Red, same as the line Not needed
WebKit (Safari's engine) Black Yes, with the CSS below

The safe pattern is two declarations in CSS. An engine that does not accept context-stroke drops the second one and keeps the first:

.head {
  fill: #555;            /* used where context-stroke is not supported */
  fill: context-stroke;  /* the head takes the colour of the line */
}

In our WebKit test, this gave a grey head instead of a black one. Where the exact colour matters in every browser, keep one marker per colour.

A finished example: a small diagram

This diagram uses two markers for everything. The arrowhead uses auto-start-reverse, so the top line is double headed with the same marker. A ring marker on marker-mid marks each corner of the route. Both use context-stroke, so recolouring a line recolours its head.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Coloured arrows from one marker</title>
<style>
  body { margin: 0; padding: 12px 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #222; }
  svg { display: block; width: 100%; max-width: 520px; margin: 0 auto; background: #fff; border-radius: 10px; }
  .box { fill: #fff; stroke: #cbd2dc; stroke-width: 1.5; }
  text { font-size: 15px; text-anchor: middle; dominant-baseline: middle; }
  .edge { fill: none; stroke-width: 2.5; }
  /* fallback colour first, then context-stroke where it is supported */
  .head { fill: #555; fill: context-stroke; }
  .ring { fill: #fff; stroke: #555; stroke: context-stroke; stroke-width: 2; }
  .swatches { display: flex; gap: 8px; justify-content: center; margin-top: 10px; flex-wrap: wrap; }
  button { font: inherit; font-size: 14px; padding: 6px 12px; border-radius: 8px; border: 1px solid #cbd2dc; background: #fff; cursor: pointer; }
</style>
</head>
<body>
<svg viewBox="0 0 400 320">
  <defs>
    <!-- one arrowhead for every colour -->
    <marker id="arrow" viewBox="0 0 10 10" refX="10" refY="5"
            markerWidth="5" markerHeight="5" orient="auto-start-reverse">
      <path class="head" d="M0 0 L10 5 L0 10 z"/>
    </marker>
    <!-- a small ring for each corner of a route -->
    <marker id="ring" viewBox="0 0 10 10" refX="5" refY="5"
            markerWidth="3" markerHeight="3">
      <circle class="ring" cx="5" cy="5" r="3.5"/>
    </marker>
  </defs>

  <rect class="box" x="20" y="20" width="120" height="50" rx="8"/><text x="80" y="45">Order</text>
  <rect class="box" x="260" y="20" width="120" height="50" rx="8"/><text x="320" y="45">Payment</text>
  <rect class="box" x="20" y="250" width="120" height="50" rx="8"/><text x="80" y="275">Refund</text>
  <rect class="box" x="260" y="250" width="120" height="50" rx="8"/><text x="320" y="275">Shipping</text>

  <!-- double-headed: the same marker on both ends -->
  <line id="e1" class="edge" x1="146" y1="45" x2="254" y2="45" stroke="#2563eb"
        marker-start="url(#arrow)" marker-end="url(#arrow)"/>
  <!-- a route with corners: marker-mid puts a ring on each corner -->
  <polyline id="e2" class="edge" points="320,76 320,130 220,130 220,190 320,190 320,244"
            stroke="#16a34a" marker-mid="url(#ring)" marker-end="url(#arrow)"/>
  <path id="e3" class="edge" d="M258 64 C 180 110 90 170 80 244" stroke="#c2410c"
        marker-end="url(#arrow)"/>
</svg>

<div class="swatches">
  <button data-c="#2563eb">Blue</button>
  <button data-c="#9333ea">Purple</button>
  <button data-c="#db2777">Pink</button>
  <button data-c="#111827">Black</button>
</div>

<script>
  const edges = ['e1', 'e2', 'e3'].map((id) => document.getElementById(id));
  let next = 0;

  // each click recolours the next arrow; the head follows the line colour
  document.querySelectorAll('button').forEach((b) =>
    b.addEventListener('click', () => {
      edges[next % edges.length].setAttribute('stroke', b.dataset.c);
      next++;
    })
  );
</script>
</body>
</html>
Each button recolours the next arrow. Where context-stroke works, the heads and rings follow the line colour.
  • Double head: both ends use the same marker.
  • Corners: marker-mid on a <polyline> rings every corner, not the ends.
  • Placement: each line stops 6 units short of its box, so the tip stays visible.

To lay out boxes and calculate the connections from their positions, see how to build a flowchart in HTML. To animate a line drawing itself before the head appears, see stroke-dasharray.

When it does not work

What you see Cause Fix
No arrowhead at all The marker is on a <rect> or <circle> Use <path>, <line>, <polyline> or <polygon>
No arrowhead, markup looks right The <marker> sits in an SVG with display: none Give that SVG zero width and height instead
Arrow points right on every line orient is missing, so it defaults to 0 Add orient="auto"
Start head points into the line orient="auto" on a start marker Use auto-start-reverse
Head is huge on thick lines markerUnits is strokeWidth Use userSpaceOnUse or a smaller markerWidth
Half the head is missing Its drawing goes outside the viewBox; markers clip it Draw inside the box, or widen the viewBox
Head is black The marker does not inherit from the line Set a fill, or context-stroke with a fallback
The line's blunt end shows past the tip Thick line with the tip exactly on the end point Move refX back a little, or end the line earlier
Another SVG's arrow shows up Two markers share one id in the page Give each marker a unique id

This one is easy to miss when markers live in a hidden sprite SVG. We tested an SVG with display: none holding the marker, and no engine drew the head. An SVG kept in the page with zero width and height worked in all three.

A diagram with arrows is easier to send than to describe. A screenshot cannot be recoloured or dragged, and an .html attachment may open as plain code on a phone. A link to the working page keeps it interactive.

To send it, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can drag the arrow and click the colours themselves. If you change the code later, the same link shows the new version.

Questions people ask

How do I add an arrowhead to an SVG line?

Define a marker inside defs with a small triangle path, give it an id, refX and refY at the tip, and orient="auto". Then add marker-end="url(#your-id)" to the line or path. The triangle is drawn on the last point and turned to follow the line.

Which SVG elements can have markers?

path, line, polyline and polygon. Shapes such as rect, circle and ellipse do not draw markers, even with marker-start or marker-end set. Draw the shape as a path if it needs them.

How do I make a double-headed arrow?

Use one marker with orient="auto-start-reverse" and set both marker-start and marker-end to it. The copy at the start is turned 180 degrees, so both heads point outward.

Why is my arrowhead black instead of the line colour?

The marker's content does not inherit from the line that uses it. It takes its colour from its own fill, or from the marker's ancestors. Set a fill on the arrowhead, use one marker per colour, or use context-stroke with a fallback colour.

Why does the arrowhead get bigger when I make the line thicker?

markerUnits defaults to strokeWidth, so markerWidth and markerHeight are measured in stroke widths. Set markerUnits="userSpaceOnUse" to size the head in the SVG's own units instead.

Keep reading