Lesson 8
Text & textPath
SVG text is fundamentally different from HTML text. In HTML, text flows - it wraps, it fills containers, it responds to layout. In SVG, text is positioned - each glyph is placed at explicit coordinates within the same coordinate system your shapes use. This makes SVG text incredibly precise (you can place individual characters exactly where you want them) but also means there's no automatic line wrapping or paragraph flow.
This lesson covers the three text elements (<text>, <tspan>, <textPath>), the positioning and alignment attributes that control where glyphs land, and how text interacts with SVG features covered in earlier lessons - paint servers, clipping/masking, and filters.
1. The <text> Element
The <text> element is SVG's equivalent of a text node. It places a string of characters at a point in the coordinate system. The key attributes:
| Attribute | Purpose | Default |
|---|---|---|
x | Horizontal position of the first glyph's anchor point | 0 |
y | Vertical position of the first glyph's baseline | 0 |
dx | Relative horizontal offset (can be a list for per-character shifts) | 0 |
dy | Relative vertical offset (can be a list for per-character shifts) | 0 |
rotate | Rotation of individual glyphs (degrees; can be a list) | none |
textLength | Desired total text length - browser adjusts spacing/glyphs to fit | auto |
lengthAdjust | How to fit: spacing (adjust gaps) or spacingAndGlyphs (stretch glyphs too) | spacing |
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg">
<text x="10" y="50" font-size="24" fill="coral">
Hello, SVG!
</text>
</svg>
The blue dot marks the anchor point (x, y). The dashed line is the baseline - notice how text sits on top of it.
Unlike CSS where top refers to the top edge of a box, SVG's y attribute on text sets the alphabetic baseline. The glyphs extend above this line. This is the single most common source of confusion with SVG text - if you set y="0", your text will be clipped because it renders above the viewport.
2. Horizontal Alignment: text-anchor
The text-anchor attribute (or CSS property) controls how text is aligned relative to its x position. Think of it as the text equivalent of CSS text-align, but anchored to a specific coordinate rather than a box edge.
| Value | Behaviour |
|---|---|
start | Text flows to the right of the anchor point (LTR). Default. |
middle | Text is centred on the anchor point. |
end | Text flows to the left - the anchor is the end of the string. |
<svg viewBox="0 0 400 140" xmlns="http://www.w3.org/2000/svg">
<!-- Vertical guide at x=200 -->
<line x1="200" y1="0" x2="200" y2="140"
stroke="grey" stroke-dasharray="4 2"/>
<text x="200" y="40" text-anchor="start"
font-size="18" fill="coral">start</text>
<text x="200" y="75" text-anchor="middle"
font-size="18" fill="coral">middle</text>
<text x="200" y="110" text-anchor="end"
font-size="18" fill="coral">end</text>
</svg>
All three texts share x="200" (blue dots). text-anchor determines which part of the string sits at that coordinate.
3. Vertical Alignment: dominant-baseline
While text-anchor handles the horizontal axis, dominant-baseline controls where the text sits vertically relative to the y coordinate. This is the attribute that solves the "my text is above the viewport" problem.
| Value | Behaviour | Use case |
|---|---|---|
auto | Same as alphabetic for horizontal text | Default - text sits on the baseline |
alphabetic | Bottom of most Latin glyphs sits on y | Normal prose text |
hanging | Top of capital letters aligns with y | When y should be the top edge |
middle | Vertical midpoint of the em-box aligns with y | Centring text on a coordinate |
central | Like middle but uses the central baseline (for CJK) | Multi-script centering |
text-top | Top of the em-box aligns with y | Precise top alignment |
text-bottom | Bottom of the em-box aligns with y | Precise bottom alignment |
<svg viewBox="0 0 400 120" xmlns="http://www.w3.org/2000/svg">
<line x1="0" y1="60" x2="400" y2="60"
stroke="grey" stroke-dasharray="4 2"/>
<text x="20" y="60" dominant-baseline="auto"
font-size="16" fill="coral">auto</text>
<text x="100" y="60" dominant-baseline="hanging"
font-size="16" fill="#61afef">hanging</text>
<text x="210" y="60" dominant-baseline="middle"
font-size="16" fill="#98c379">middle</text>
<text x="310" y="60" dominant-baseline="text-top"
font-size="16" fill="#e5c07b">text-top</text>
</svg>
All four texts share y="60" (the dashed line). dominant-baseline controls which part of the glyph sits at that y.
To perfectly centre text at a coordinate (e.g., labelling a circle), combine text-anchor="middle" with dominant-baseline="middle". This centres horizontally and vertically - the equivalent of display: flex; align-items: center; justify-content: center in CSS, but for a single point.
4. The <tspan> Element
<tspan> is to SVG text what <span> is to HTML - it marks up a sub-portion of text so you can style or reposition it independently. It must be a child of <text> or another <tspan>.
Styling sub-portions
<svg viewBox="0 0 400 80" xmlns="http://www.w3.org/2000/svg">
<text x="10" y="45" font-size="20" fill="var(--text)">
This is <tspan fill="#e06c75" font-weight="bold">bold and red</tspan>
and this is normal.
</text>
</svg>
<tspan> changes fill and weight mid-string without breaking text flow.
Per-character positioning with dx/dy
The dx and dy attributes accept a space-separated list of values, one per character. Each value shifts that character relative to where it would naturally sit. This lets you create manual kerning or wave effects.
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg">
<text x="30" y="50" font-size="28" fill="coral">
<tspan dy="0 -8 -14 -8 0 8 14 8 0">WAVE TEXT</tspan>
</text>
</svg>
Each value in dy shifts one character vertically relative to the previous.
The key difference is glyph orientation. A dy list shifts each character's position but every glyph stays perfectly upright - an "l" always points straight up. With <textPath>, each glyph rotates to stay tangent to the curve at its anchor point - letters lean into the path like carriages on a railway track. For text that should genuinely follow a curve, <textPath> looks natural; dy displacement looks like letters bouncing on a trampoline. Reserve dy lists for micro-adjustments, manual kerning, or jitter effects where you explicitly don't want rotation.
Multi-line text with tspan
SVG has no automatic line wrapping. To simulate multiple lines, use <tspan> elements with x (to reset to the left) and dy (to move down by the line height):
<svg viewBox="0 0 400 120" xmlns="http://www.w3.org/2000/svg">
<text x="20" font-size="18" fill="coral">
<tspan x="20" y="30">First line of text</tspan>
<tspan x="20" dy="1.4em">Second line of text</tspan>
<tspan x="20" dy="1.4em">Third line of text</tspan>
</text>
</svg>
Setting x on each <tspan> resets the horizontal position. dy="1.4em" creates line spacing.
SVG 1.1 has no built-in word wrap. SVG 2 adds inline-size for auto-wrapping, but browser support is still incomplete (Chrome/Edge support it; Firefox/Safari partial as of 2026). For cross-browser multi-line text, use the <tspan> technique above, or embed HTML via <foreignObject>.
5. Font Properties
SVG text supports the same font properties as CSS. These can be set as presentation attributes on the element or via CSS (which takes precedence per the cascade rules covered in Lesson 4).
| Property/Attribute | Purpose | Example |
|---|---|---|
font-family | Typeface stack | "Georgia, serif" |
font-size | Glyph size (user units, px, em, %) | 24 |
font-weight | Boldness | bold or 700 |
font-style | Italic / oblique | italic |
font-variant | Small-caps, etc. | small-caps |
font-stretch | Condensed / expanded | condensed |
letter-spacing | Space between characters | 2 |
word-spacing | Space between words | 5 |
text-decoration | Underline / strikethrough | underline |
A font-size="24" means 24 user units - the same units your shapes use. If your viewBox is 400 units wide and the viewport is 800px wide, that "24" becomes 48 screen pixels. The font scales with the viewBox just like everything else in SVG. This is why SVG text stays crisp at any zoom level.
Loading custom fonts
SVG uses the same @font-face mechanism as HTML. Place a <style> block inside your SVG and declare fonts exactly as you would in CSS:
<!-- Option 1: Google Fonts via @import -->
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg">
<style>
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700&display=swap');
</style>
<text x="200" y="60" text-anchor="middle"
font-family="Playfair Display" font-size="32"
fill="coral">Custom Font</text>
</svg>
<!-- Option 2: Self-hosted @font-face -->
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg">
<style>
@font-face {
font-family: 'MyFont';
src: url('./fonts/myfont.woff2') format('woff2');
}
</style>
<text x="20" y="60" font-family="MyFont"
font-size="28" fill="coral">Self-hosted</text>
</svg>
<!-- Option 3: Base64-embedded (fully self-contained) -->
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg">
<style>
@font-face {
font-family: 'Embedded';
src: url(data:font/woff2;base64,d09GMgABA...) format('woff2');
}
</style>
<text x="20" y="60" font-family="Embedded"
font-size="28" fill="coral">No network requests</text>
</svg>
The rules change based on context:
- Inline SVG in HTML - inherits the page's
@font-facerules automatically. No need to redeclare. <object>,<iframe>,<embed>- the SVG loads as a full document. External font URLs work fine.<img>tag - blocks all external fetches for security. Only base64-embedded fonts work here.- CSS
background-image- same restrictions as<img>.
For self-contained animated SVGs, external font URLs create a dependency on network availability. The robust approach is to subset the font (include only the characters you use) and embed it as base64. Tools like fonttools/pyftsubset or Transfonter can subset and base64-encode in one step. A subsetted display font for a few words typically adds only 5–15 KB.
Text as paint target
Text can be filled and stroked just like shapes. This means you can apply gradients, patterns, and even use text as a clip path:
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="text-grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="#e06c75"/>
<stop offset="50%" stop-color="#e5c07b"/>
<stop offset="100%" stop-color="#61afef"/>
</linearGradient>
</defs>
<text x="200" y="65" text-anchor="middle" font-size="48"
font-weight="bold" fill="url(#text-grad)"
stroke="#333" stroke-width="1">GRADIENT</text>
</svg>
Text filled with a linear gradient and stroked - treated exactly like any other shape.
6. The <textPath> Element
This is where SVG text gets exciting for animation work. <textPath> renders text along the contour of a path. The glyphs follow the curves, rotate to stay tangent to the path, and can be offset along it.
The path itself is usually hidden (placed in <defs>). The text's baseline follows the path's contour - each glyph rotates to remain perpendicular to the path at its position. The startOffset attribute slides the text along the path like a slider.
Basic usage
<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="curve" d="M 30,150 Q 200,20 370,150" fill="none"/>
</defs>
<!-- Show the path for reference -->
<use href="#curve" stroke="grey" stroke-dasharray="4 2"/>
<text font-size="18" fill="coral">
<textPath href="#curve">
Text flowing along a quadratic curve
</textPath>
</text>
</svg>
The text's baseline follows the arc of the quadratic Bézier. Each glyph rotates to stay tangent.
textPath attributes
| Attribute | Purpose | Default |
|---|---|---|
href | Reference to a <path> element (by ID) | required |
startOffset | Distance along the path where text begins (length, %, or number) | 0 |
method | align (glyphs rotate to tangent) or stretch (glyphs distort to fit) | align |
spacing | auto or exact - how inter-glyph spacing is handled on curves | exact |
side | left or right - which side of the path the text renders on | left |
textLength | Desired total length of the text (overrides natural length) | auto |
lengthAdjust | How to fit: spacing or spacingAndGlyphs | spacing |
startOffset - sliding text along a path
startOffset is the key to animating text on a path. It defines where the first character begins, measured from the start of the path. When animated, it creates a scrolling-marquee effect along the curve.
<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="oval" d="M 50,100 A 150,60 0 1 1 350,100 A 150,60 0 1 1 50,100"
fill="none"/>
</defs>
<text font-size="16" fill="#61afef">
<textPath href="#oval" startOffset="0%">
Orbiting text along an ellipse — SMIL can animate startOffset!
<animate attributeName="startOffset" from="0%" to="100%"
dur="8s" repeatCount="indefinite"/>
</textPath>
</text>
</svg>
A live SMIL animation - startOffset cycles from 0% to 100%, making text orbit the ellipse. This is a good example of a self-contained, declarative SVG animation.
Circular text (a common pattern)
A full circle path lets you wrap text around a badge or logo. Combine with text-anchor="middle" and startOffset="50%" to centre the text at the top of the circle:
<svg viewBox="0 0 300 300" xmlns="http://www.w3.org/2000/svg">
<defs>
<path id="circle-path"
d="M 150,50 A 100,100 0 1 1 149.99,50" fill="none"/>
</defs>
<circle cx="150" cy="150" r="100" fill="none"
stroke="grey" stroke-dasharray="4 2"/>
<text font-size="18" fill="coral" text-anchor="middle">
<textPath href="#circle-path" startOffset="50%">
CIRCULAR TEXT DEMO
</textPath>
</text>
</svg>
Text centred at the top of a circle. The path starts at 12 o'clock; startOffset="50%" combined with text-anchor="middle" centres the string there.
7. textLength and lengthAdjust
The textLength attribute tells the browser how long the text should be. If the natural rendering is shorter or longer, the browser adjusts to fit. lengthAdjust controls how it adjusts.
| lengthAdjust value | What gets adjusted | Visual effect |
|---|---|---|
spacing | Only the gaps between glyphs | Characters keep their shape; tracking changes |
spacingAndGlyphs | Both gaps and glyph widths | Characters stretch or compress horizontally |
<svg viewBox="0 0 400 160" xmlns="http://www.w3.org/2000/svg">
<!-- Natural length -->
<text x="20" y="35" font-size="20" fill="coral">Stretched</text>
<!-- spacing only -->
<text x="20" y="75" font-size="20" fill="#61afef"
textLength="350" lengthAdjust="spacing">Stretched</text>
<!-- spacing and glyphs -->
<text x="20" y="115" font-size="20" fill="#98c379"
textLength="350" lengthAdjust="spacingAndGlyphs">Stretched</text>
<text x="20" y="150" font-size="10" fill="grey">
Top: natural | Middle: spacing | Bottom: spacingAndGlyphs
</text>
</svg>
All three say "Stretched" at the same font-size. textLength="350" forces them wider; lengthAdjust controls whether glyphs distort.
8. The rotate Attribute
The rotate attribute on <text> or <tspan> rotates individual glyphs (not the entire text block). Like dx/dy, it accepts a list of values - one per character. If fewer values than characters, the last value applies to all remaining characters.
<svg viewBox="0 0 400 100" xmlns="http://www.w3.org/2000/svg">
<!-- Values are degrees (no unit suffix). Positive = clockwise. -->
<text x="30" y="60" font-size="28" fill="coral"
rotate="0 5 10 15 20 25 30 25 20 15 10 5 0">TUMBLING TEXT</text>
</svg>
Each character tilts independently. Combine small random rotate values with small dy jitters to simulate handwriting - characters that are slightly wonky and uneven, like real pen strokes.
Don't confuse the rotate attribute with a transform="rotate(...)" on the text element. The attribute rotates each glyph individually around its own position; the transform rotates the entire text block around a point. They serve very different purposes.
9. Text and Transforms
Everything from Lesson 2 applies to text. You can translate, rotate, scale, and skew text elements using the transform attribute. A particularly useful pattern is combining text with <g> transforms for layout:
<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<!-- Vertical text via rotation -->
<text x="0" y="0" font-size="16" fill="coral"
transform="translate(30, 180) rotate(-90)">
Vertical label
</text>
<!-- Scaled text -->
<text x="80" y="100" font-size="16" fill="#61afef"
transform="scale(1, 2)">Tall text</text>
</svg>
Transforms on text work identically to transforms on shapes - they manipulate the coordinate system.
10. Text and Other SVG Features
Text elements participate fully in SVG's rendering pipeline. You can:
- Clip with text - use
<text>inside a<clipPath>to clip images/shapes to the letterforms - Mask with text - text luminance drives alpha in a
<mask> - Filter text - apply any filter from Lesson 7 to text
- Fill with patterns -
fill="url(#pattern)"works on text - Animate text - SMIL
<animate>on text attributes (coming in Lesson 9)
Text as a clipping path
<svg viewBox="0 0 400 120" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="text-clip">
<text x="200" y="80" text-anchor="middle"
font-size="60" font-weight="bold">CLIPPED</text>
</clipPath>
<linearGradient id="clip-bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#e06c75"/>
<stop offset="100%" stop-color="#61afef"/>
</linearGradient>
</defs>
<rect width="400" height="120" fill="url(#clip-bg)"
clip-path="url(#text-clip)"/>
</svg>
A gradient rectangle clipped to text letterforms. The text itself isn't rendered - it's used purely as a geometric clipping shape.
11. Accessibility and Semantics
SVG text is selectable and searchable by default - unlike text in Canvas or rasterised images. Screen readers can read it. Search engines can index it. This is one of SVG's great advantages for text in graphics.
However, there are a few things to be aware of:
<title>and<desc>on the parent<svg>provide accessible names and descriptions for the entire graphic.- Decorative text (logos, artistic effects) that duplicates information available elsewhere should have
aria-hidden="true". - Text inside
<clipPath>or<mask>is never rendered and thus not accessible - ensure the visible text or an ARIA label conveys the same information.
12. Common Patterns & Recipes
Centred label inside a shape
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<circle cx="100" cy="100" r="80" fill="#282c34" stroke="#61afef"/>
<text x="100" y="100"
text-anchor="middle" dominant-baseline="middle"
font-size="20" fill="#e0e4eb">Centred</text>
</svg>
The most common pattern: text-anchor="middle" + dominant-baseline="middle" centres text at a point.
Animated typing effect with textLength
<svg viewBox="0 0 400 80" xmlns="http://www.w3.org/2000/svg">
<text x="20" y="50" font-size="24" fill="coral"
textLength="0" lengthAdjust="spacing">
Hello, World!
<animate attributeName="textLength"
from="0" to="300" dur="3s"
fill="freeze"/>
</text>
</svg>
Animating textLength from 0 expands the text character spacing - a reveal effect using pure SMIL.
13. <foreignObject> - Embedding HTML in SVG
When SVG's text system isn't enough (you need word wrap, rich formatting, form inputs), <foreignObject> lets you embed arbitrary HTML inside an SVG document:
<svg viewBox="0 0 400 200" xmlns="http://www.w3.org/2000/svg">
<!-- SVG content -->
<rect width="400" height="200" rx="10" fill="#282c34"/>
<!-- Embedded HTML with auto word-wrapping -->
<foreignObject x="20" y="20" width="360" height="160">
<div xmlns="http://www.w3.org/1999/xhtml"
style="color: #e0e4eb; font: 14px sans-serif;">
<h3 style="margin: 0 0 8px;">Rich HTML content</h3>
<p>This text wraps automatically, supports
<strong>bold</strong>, <em>italic</em>, links,
and any HTML you want.</p>
</div>
</foreignObject>
</svg>
<foreignObject> embeds a full HTML rendering context inside the SVG. The text wraps naturally.
| Attribute | Purpose |
|---|---|
x, y | Position of the HTML viewport within the SVG |
width, height | Size of the HTML rendering area |
<foreignObject> is powerful but has constraints: it doesn't work in SVGs loaded via <img> (blocked for security), it can't be animated with SMIL, and its content can't be clipped/masked by SVG clip paths. It's best used in inline SVGs or <object>-embedded SVGs where you need HTML layout capabilities (paragraphs, form controls, iframes).
14. The SVG <a> Element
SVG has its own <a> element - it works like the HTML anchor but wraps graphical content instead of text:
<svg viewBox="0 0 200 80" xmlns="http://www.w3.org/2000/svg">
<a href="https://developer.mozilla.org/en-US/docs/Web/SVG">
<rect x="20" y="15" width="160" height="50" rx="8"
fill="#61afef"/>
<text x="100" y="47" text-anchor="middle"
font-size="16" fill="white">MDN SVG Docs</text>
</a>
</svg>
Click the button - it's a real link (opens MDN in a new tab). The <a> wraps the rect and text, making both clickable.
The SVG <a> supports href, target, and download - same as HTML. It's natively focusable (keyboard-navigable with Tab) without needing tabindex. Its children get a pointer cursor automatically.
Use SVG's <a> when you want graphical SVG elements to be clickable links within the SVG itself (especially for standalone .svg files). For inline SVGs in HTML, you could also wrap the entire <svg> in an HTML <a> instead. The SVG <a> gives finer control - different shapes within one SVG can link to different URLs.
Quiz
Question 1
What does the y attribute on a <text> element position?
Question 2
To perfectly centre text at a coordinate point, which combination of attributes do you need?
Question 3
What does startOffset="50%" on a <textPath> do?
Question 4
How do you create multi-line text in SVG 1.1 (cross-browser)?
Question 5
What is the difference between lengthAdjust="spacing" and lengthAdjust="spacingAndGlyphs"?
Exercises
Try these in your editor or a CodePen to build muscle memory:
- Label a bar chart - Create three horizontal
<rect>bars and place centred text labels inside each one usingtext-anchor="middle"anddominant-baseline="middle". - Badge text - Create a circular badge with text wrapping around the top ("CERTIFIED") and bottom ("PREMIUM") using two
<textPath>elements on circle paths. Hint: useside="right"or flip the path direction for bottom text. - Animated marquee - Put text on a straight horizontal path and animate
startOffsetfrom100%to-100%with SMIL for a ticker-tape effect. - Text clipping - Use bold text as a
<clipPath>and clip an image or animated gradient through the letterforms. - Wave text - Use
dywith a sinusoidal pattern of values to make text undulate. Try animating thedyvalues for a "breathing" wave effect.
<text>, <tspan>, and <textPath> with live examples. The W3C SVG 1.1 Text chapter is the definitive reference for edge cases and formal definitions.