← Back to Index

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:

AttributePurposeDefault
xHorizontal position of the first glyph's anchor point0
yVertical position of the first glyph's baseline0
dxRelative horizontal offset (can be a list for per-character shifts)0
dyRelative vertical offset (can be a list for per-character shifts)0
rotateRotation of individual glyphs (degrees; can be a list)none
textLengthDesired total text length - browser adjusts spacing/glyphs to fitauto
lengthAdjustHow 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>
Hello, SVG! anchor point (10, 50)

The blue dot marks the anchor point (x, y). The dashed line is the baseline - notice how text sits on top of it.

y positions the baseline, not the top

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.

ValueBehaviour
startText flows to the right of the anchor point (LTR). Default.
middleText is centred on the anchor point.
endText 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>
start middle end

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.

ValueBehaviourUse case
autoSame as alphabetic for horizontal textDefault - text sits on the baseline
alphabeticBottom of most Latin glyphs sits on yNormal prose text
hangingTop of capital letters aligns with yWhen y should be the top edge
middleVertical midpoint of the em-box aligns with yCentring text on a coordinate
centralLike middle but uses the central baseline (for CJK)Multi-script centering
text-topTop of the em-box aligns with yPrecise top alignment
text-bottomBottom of the em-box aligns with yPrecise 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>
auto hanging middle text-top

All four texts share y="60" (the dashed line). dominant-baseline controls which part of the glyph sits at that y.

Centering text on a point

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>
This is bold and red and this is normal.

<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>
WAVE TEXT

Each value in dy shifts one character vertically relative to the previous.

dy lists vs textPath for wave effects

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>
First line of text Second line of text Third line of text

Setting x on each <tspan> resets the horizontal position. dy="1.4em" creates line spacing.

No automatic text wrapping

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/AttributePurposeExample
font-familyTypeface stack"Georgia, serif"
font-sizeGlyph size (user units, px, em, %)24
font-weightBoldnessbold or 700
font-styleItalic / obliqueitalic
font-variantSmall-caps, etc.small-caps
font-stretchCondensed / expandedcondensed
letter-spacingSpace between characters2
word-spacingSpace between words5
text-decorationUnderline / strikethroughunderline
font-size in SVG uses the user coordinate system

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>
Font loading depends on how the SVG is embedded

The rules change based on context:

  • Inline SVG in HTML - inherits the page's @font-face rules 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>.
Self-contained SVGs need embedded fonts

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>
GRADIENT

Text filled with a linear gradient and stroked - treated exactly like any other shape.

Ad

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.

textPath renders glyphs along a path's stroke

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>
Text flowing along a quadratic curve

The text's baseline follows the arc of the quadratic Bézier. Each glyph rotates to stay tangent.

textPath attributes

AttributePurposeDefault
hrefReference to a <path> element (by ID)required
startOffsetDistance along the path where text begins (length, %, or number)0
methodalign (glyphs rotate to tangent) or stretch (glyphs distort to fit)align
spacingauto or exact - how inter-glyph spacing is handled on curvesexact
sideleft or right - which side of the path the text renders onleft
textLengthDesired total length of the text (overrides natural length)auto
lengthAdjustHow to fit: spacing or spacingAndGlyphsspacing

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 &mdash; SMIL can animate startOffset!
      <animate attributeName="startOffset" from="0%" to="100%"
               dur="8s" repeatCount="indefinite"/>
    </textPath>
  </text>
</svg>
Orbiting text along an ellipse - SMIL can animate startOffset!

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>
CIRCULAR TEXT DEMO

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 valueWhat gets adjustedVisual effect
spacingOnly the gaps between glyphsCharacters keep their shape; tracking changes
spacingAndGlyphsBoth gaps and glyph widthsCharacters 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>
Stretched Stretched Stretched Top: natural | Middle: spacing only | Bottom: spacingAndGlyphs

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>
TUMBLING TEXT

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.

rotate vs transform

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>
Vertical label Tall text Tilted

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:

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>
CLIPPED

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:

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>
Centred

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>
Hello, World!

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>

Rich HTML content

This text wraps automatically, supports bold, italic, and any HTML you want. It flows like a normal paragraph - something SVG's <text> element cannot do.

<foreignObject> embeds a full HTML rendering context inside the SVG. The text wraps naturally.

AttributePurpose
x, yPosition of the HTML viewport within the SVG
width, heightSize of the HTML rendering area
foreignObject limitations

<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>
MDN SVG Docs

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.

When to use SVG <a> vs HTML <a>

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?

The top edge of the tallest glyph
The alphabetic baseline of the text
The vertical centre of the em-box
The bottom edge of descender glyphs

Question 2

To perfectly centre text at a coordinate point, which combination of attributes do you need?

text-anchor="start" and dominant-baseline="hanging"
text-anchor="middle" and dominant-baseline="auto"
text-anchor="middle" and dominant-baseline="middle"
text-anchor="end" and dominant-baseline="central"

Question 3

What does startOffset="50%" on a <textPath> do?

Scales the text to half its normal size
Skips the first half of the text string
Starts rendering text at the midpoint of the referenced path
Offsets the text 50% perpendicular to the path

Question 4

How do you create multi-line text in SVG 1.1 (cross-browser)?

Use the white-space: pre-wrap CSS property on the text element
Use multiple tspan elements with x (to reset) and dy (to advance)
Insert newline characters directly into the text content
Set the inline-size attribute on the text element for wrapping

Question 5

What is the difference between lengthAdjust="spacing" and lengthAdjust="spacingAndGlyphs"?

spacing adjusts word gaps; spacingAndGlyphs adjusts letter gaps
spacing only changes inter-glyph gaps; spacingAndGlyphs also stretches the glyphs themselves
spacing works on single lines; spacingAndGlyphs works on multiline text
spacing preserves text-anchor; spacingAndGlyphs ignores text-anchor

Exercises

Try these in your editor or a CodePen to build muscle memory:

  1. Label a bar chart - Create three horizontal <rect> bars and place centred text labels inside each one using text-anchor="middle" and dominant-baseline="middle".
  2. Badge text - Create a circular badge with text wrapping around the top ("CERTIFIED") and bottom ("PREMIUM") using two <textPath> elements on circle paths. Hint: use side="right" or flip the path direction for bottom text.
  3. Animated marquee - Put text on a straight horizontal path and animate startOffset from 100% to -100% with SMIL for a ticker-tape effect.
  4. Text clipping - Use bold text as a <clipPath> and clip an image or animated gradient through the letterforms.
  5. Wave text - Use dy with a sinusoidal pattern of values to make text undulate. Try animating the dy values for a "breathing" wave effect.