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>| Attribute | Controls | Values |
|---|---|---|
x / y | The position of the baseline | User units, or a list per glyph |
dx / dy | Offsets from the current position | User units |
text-anchor | Horizontal alignment at x | start, middle, end |
dominant-baseline | Which part of the glyph sits at y | hanging, middle, central, text-after-edge |
font-size | Text size | User units, so it scales with the viewBox |
letter-spacing | Extra space between glyphs | User units; can be negative |
textLength + lengthAdjust | Force the text to a width | spacing 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>hrefis the modern attribute;xlink:hrefis the legacy form. Include both only if you must support a very old browser.startOffsetaccepts a length or a percentage of the path's total length, so50%plustext-anchor: middlecentres 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 atextPathtarget.
/* 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>| Approach | Works in | Cost | Fits |
|---|---|---|---|
| Page's CSS font | No | None extra | Inline SVG |
@font-face inside the SVG | Yes | Tens to hundreds of KB | Standalone exportable graphics |
| Text converted to paths | Yes | Larger file, no longer editable text | Logos and wordmarks |
| Generic families only | Yes | Inconsistent across systems | Never for a brand |
foreignObject with HTML | Yes, with caveats | Full HTML layout available | Complex 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>foreignObjectis 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.Related
Paths in depth: commands, curves and arcs Symbol sprites, icon systems and reuse
Last refreshed 2026-09-18.