Text, fonts and text on a path

Lay out text with tspan, control baselines, put text on a curve with textPath, and handle font loading honestly.

text, tspan and positioning

<svg viewBox="0 0 320 160" xmlns="http://www.w3.org/2000/svg">
  <!-- x and y are the baseline of the FIRST line, not the top of the box. -->
  <text x="10" y="30" font-family="system-ui" font-size="18" fill="#0f172a">
    Simple text at a baseline
  </text>

  <!-- tspan: multiple runs inside one text element -->
  <text x="10" y="70" font-size="16" fill="#334155">
    Total:
    <tspan font-weight="700" fill="#6d28d9">1,290</tspan>
    <tspan dx="8" fill="#64748b">this quarter</tspan>
  </text>

  <!-- dy on a tspan moves to the next line. x resets the horizontal position. -->
  <text x="10" y="100" font-size="14" fill="#334155">
    <tspan x="10" dy="0">First line</tspan>
    <tspan x="10" dy="20">Second line</tspan>
    <tspan x="10" dy="20" fill="#0ea5e9">Third line, in blue</tspan>
  </text>

  <!-- text-anchor controls horizontal alignment relative to x -->
  <g font-size="12" fill="#0f172a">
    <circle cx="160" cy="140" r="2" fill="#ef4444"/>
    <text x="160" y="138" text-anchor="start">start</text>
    <text x="160" y="152" text-anchor="middle">middle</text>
    <text x="160" y="166" text-anchor="end">end</text>
  </g>
</svg>
AttributeControlsValues
x / yThe position of the baselineUser units, or a list per glyph
dx / dyOffsets from the current positionUser units
text-anchorHorizontal alignment at xstart, middle, end
dominant-baselineWhich part of the glyph sits at yhanging, middle, central, text-after-edge
font-sizeText sizeUser units, so it scales with the viewBox
letter-spacingExtra space between glyphsUser units; can be negative
textLength + lengthAdjustForce the text to a widthspacing or spacingAndGlyphs
⚠️
font-size in SVG is in user units, so it scales with the viewBox. A 14-unit font in a viewBox that is scaled down to 14 pixels is unreadable. If the text must stay legible at every size, either keep the text out of the scaled graphic or use CSS with a fixed pixel size on a non-scaling container.

Text on a path

<svg viewBox="0 0 320 200" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <!-- The path is referenced by id and never drawn itself. -->
    <path id="wave" d="M20 160 C 90 80, 230 80, 300 160" fill="none"/>
    <path id="circle-path" d="M160 100 m-70 0 a70 70 0 1 1 140 0 a70 70 0 1 1 -140 0" fill="none"/>
  </defs>

  <!-- textPath puts the glyphs along the path. startOffset positions them. -->
  <text font-size="16" fill="#6d28d9" font-family="system-ui">
    <textPath href="#wave" startOffset="0" side="left">
      Text that follows the curve
    </textPath>
  </text>

  <!-- A circular label: the classic use -->
  <text font-size="12" font-family="system-ui" fill="#334155" letter-spacing="2">
    <textPath href="#circle-path" startOffset="0%">SVG TEXT ALONG A CIRCULAR PATH</textPath>
  </text>

  <!-- Centring: use a percentage of the path length -->
  <text font-size="14" fill="#0ea5e9" font-family="system-ui" text-anchor="middle">
    <textPath href="#wave" startOffset="50%">centred on the curve</textPath>
  </text>
</svg>
  • href is the modern attribute; xlink:href is the legacy form. Include both only if you must support a very old browser.
  • startOffset accepts a length or a percentage of the path's total length, so 50% plus text-anchor: middle centres the run.
  • The path itself is normally in <defs> with no stroke, so it is referenced but invisible.
  • A closed circular path needs two arcs (or one arc with a large sweep) — a single <circle> element cannot be a textPath target.
/* The truly useful trick: a static layout with dynamic text.
   Put the tspan in a slot the page fills. */
.variable-label {
  font-family: system-ui;
  font-size: 4px;
  fill: currentColor;
}

/* Fit text to a fixed width regardless of the string's real width. */
.fit-to-width {
  /* textLength and lengthAdjust are attributes, not CSS properties */
}

/* Responsive typography inside an SVG that scales: use a viewBox-matched
   font size so the ratio is preserved, and clamp the container in CSS. */
.icon-with-label { width: clamp(120px, 30vw, 320px); height: auto; }

Font loading and the alternatives

<!-- A web font must be loaded before the SVG renders, or the text reflows
     after the fact — and in an <img> or background SVG, the page's fonts are
     not available at all. -->
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>

<svg viewBox="0 0 200 40" xmlns="http://www.w3.org/2000/svg">
  <defs>
    <!-- Embedding a font in the SVG makes the file standalone but huge. -->
    <!-- <style>@font-face { font-family: Inter; src: url(data:font/woff2;base64,...); }</style> -->
  </defs>
  <text x="0" y="24" font-family="Inter, system-ui, sans-serif">Embedded and standalone</text>
</svg>
ApproachWorks in CostFits
Page's CSS fontNoNone extraInline SVG
@font-face inside the SVGYesTens to hundreds of KBStandalone exportable graphics
Text converted to pathsYesLarger file, no longer editable textLogos and wordmarks
Generic families onlyYesInconsistent across systemsNever for a brand
foreignObject with HTMLYes, with caveatsFull HTML layout availableComplex text blocks in a standalone file
<!-- foreignObject embeds HTML inside SVG. Powerful, and a compatibility trap. -->
<svg viewBox="0 0 300 120" xmlns="http://www.w3.org/2000/svg" width="300" height="120">
  <rect x="0" y="0" width="300" height="120" fill="#f8fafc" rx="8"/>
  <foreignObject x="12" y="12" width="276" height="96">
    <!-- Every element inside must be in the XHTML namespace. -->
    <div xmlns="http://www.w3.org/1999/xhtml" style="font:13px system-ui;color:#0f172a">
      <p style="margin:0 0 6px;font-weight:600">Rendered as HTML</p>
      <p style="margin:0;color:#475569">Reflows, wraps and honours the page's CSS.</p>
    </div>
  </foreignObject>
</svg>
  • foreignObject is not rendered at all in some SVG renderers, including many image conversion tools. It is reliable in browsers and unreliable everywhere else.
  • Text converted to paths cannot be selected, searched or read by a screen reader, and it cannot be translated. Use it for logos only, and keep an accessible name on the element.
  • In an SVG loaded as an <img> or a CSS background, the page's fonts, CSS variables and scripts do not apply. That is the constraint that decides the embedding method.
  • A slowly loading font causes a visible reflow of centred text. Preload the font file, or accept the shift and reserve the space.

FAQ

Why is my text cut off at the top?
y is the baseline, not the top of the text. Either add roughly 0.8 times the font size to y, or set dominant-baseline: hanging (or text-before-edge) so the top of the first line lands at y.
Why does textPath render nothing?
The referenced path is missing, or it is inside a different SVG document. Inside the same document, check that the path's id matches the href, that the path has a real geometry (a path with only M has no length), and that the text element is not empty.

Paths in depth: commands, curves and arcs Symbol sprites, icon systems and reuse

Last refreshed 2026-09-18.