Home » Blog » Art-Directed Text in SVGs: Lessons in Accessibility & Browser Compatability

Art-Directed Text in SVGs: Lessons in Accessibility & Browser Compatability

I’ve had the amazing opportunity to develop a website for a company started by two legally blind brothers. Their brand sells t-shirts that prioritize feel and softness, and all their profits go back into finding a cure for blindness. The agency that designed their site and branding went for an “eye chart” look for section headers and other text assets; meaning, per line, the font size can vary, but all lines should be equally justified.

an eye chart with justified text
Example eye chart
Example art directed headline from the project

As the developer, it was my job to not only recreate these banners as closely as possible across browsers and devices, but also respect the accessibility needs of visually impaired users—especially important considering the client! I ended up using slabText with some modifications for areas where the client would need to update text. But slabText is, by nature, a dependency, and I wanted tighter control over inline elements that would not be changing.

So I asked the designer to send me SVGs of the headers, exported as shapes.  My initial thoughts were to add alt text in the wrapper and/or visually hidden content just outside of it so that screen readers would pick up the text. After skimming this article on making SVGs more accessible, I learned that you can use live text right inside SVGs and control, line by line, letter by letter, font face, kerning, line height, and more.

Meaning, unlike shapes, text inside an SVG wrapper would be, findable, selectable, readable, zoomable, and SEO-friendly, as well as scaleable, readable, and art directed.

The clouds parted!! And thus began my long and instructive journey on controlling text inside SVGs.

SVG Text Basics

I’ll assume basic knowledge of SVGs, and that’s all about I have, anyway. The W3C <text> spec is here.

SVG’s text elements are rendered like other graphics elements. Thus, coordinate system transformations, painting, clipping and masking features apply to text elements in the same way as they apply to shapes such as paths and rectangles.
SVG 1.1 (Second Edition), W3C

At the most basic level, text inside SVGs looks like this (the blue box shows the boundaries of the SVG):

<svg width="350" height="200" viewBox="0 0 350 200">
 <text x="0" y="100">I am a single line of text inside an SVG.</text>
</svg>

Which would render to this:

Chrome Firefox
Safari IE 11

Happily this simple example renders the same across browsers.

Text Position

Like any other element inside an SVG, text must be deliberately placed on the coordinate plane. Removing x and y values would give us this in Chrome, Safari, and Firefox:

<text> without x or y positioning

…which is the same as if we had given this element a position of x=0 y=0. Text can take positive or negative integers. Here is the example with x="200" y="180":

 Chrome, Safari & Firefox IE 11
   

Chrome, Safari, and Firefox default to overflow: hidden for all SVGs, meaning, anything that escapes the container will be clipped. In IE, the overflow is visible.

Lessons learned:

  • <text> elements must be deliberately placed on the coordinate system, with the y value being the line’s baseline coordinate
  • Character descenders will overflow of the line’s y position is all the way at the bottom of the viewbox
  • overflow must be set in the stylesheet to either visible or hidden for consistent cross-browser behavior

Basic Styling

<text> can be treated with font faces, weights, and so on, either inline, with classes, or definitions. My examples moving forward will all be inline.

<svg width="350" height="200" viewBox="0 0 350 200"> 
   <text x="0" y="150" font-family="Times New Roman" font-weight="bold" font-size="20">
       I am a single line of text inside an SVG.</text>
</svg>

font-size can either be a unitless value, or px, pt, em, or rem. A unitless value is assumed to be height units of the coordinate plane, eg, 20 = 20 units high. Using unitless values can give you precise control over how much of the canvas the font covers. Notice in the example above, I bumped up the y value to 150 from 100 to lower the baseline.

Text within the viewbox does not scale down to fit, but overflows. Here is an example where overflow behavior is not set:

 Chrome, Safari & Firefox IE 11
   

Lessons learned:

  • font-size can be related to the viewbox or the root document
  • y position must be adjusted to account for font-size changes
  • Characters that are too long for the viewbox as a result of a large font-size will overflow

Letter Spacing

Letter spacing gets its own section as it behaves so differently across browsers.

 <svg width="350" height="200" viewBox="0 0 350 200">
   <text x="0" y="100" letter-spacing="5">I am a single line of text inside an SVG.</text>
 </svg>
Chrome & Safari Firefox IE 11

As you can see, Firefox does not accept letter-spacing at all.

I don’t know why this is. There is another attribute called textLength which can help us here…in some circumstances. Its values are unit-less and are defined by the coordinate plane:

 <svg width="350" height="200" viewBox="0 0 350 200">
   <text x="0" y="100" textLength="350">I am a single line of text inside an SVG.</text>
 </svg>

I am telling the text to take up all the space from 0 to 350 of the viewbox. The letter-spacing will adjust automatically to condense or expand the kerning, no matter how many glyphs are in the line. It looks the same across browsers.

Two lines of each, each with textLength=”350″ but the second example crams more glyphs into the same space.
 <svg width="350" height="200" viewBox="0 0 350 200">
   <text x="0" y="100" textLength="350">I am a single line of text inside an SVG. I am a single line of text inside an SVG.</text>
 </svg>
 <svg width="350" height="200" viewBox="0 0 350 200">
   <text x="0" y="100" textLength="350">I am a single line.</text>
</svg>

Lessons learned:

  • Letter-spacing is not cross-browser compatible
  • textLength works on <text> elements on all browsers and will condense or expand text to fit

This is where I thought I hit gold for my eyechart-style headers:

But a problem arises when you have multi-lines of text.

Multiple Lines of Text & <tspan>

<text> is sort of the same as a <p> element. Each <text> chunk will be read out as a paragraph. So this…

<svg width="350" height="200" viewBox="0 0 350 200">
  <text x="0" y="80" textLength="350">I am a single line of text inside an SVG</text>
  <text x="0" y="100" textLength="350">I am yet another line of text.</text>
</svg>

….is all well and good when you can semantically treat each line as a paragraph. But for my  purposes, each line is part of a single heading.

The development for a cure simply requires time and funding
This example headline is one phrase on four lines.

That’s where <tspan> comes in. Much like <span>, it’s a non-semantic element used to wrap text for positioning and styling. Notice how each line has its own y value.

<svg width="350" height="200" viewBox="0 0 350 200">
  <text>
   <tspan x="1" y="80">I am a single line of text inside an SVG</tspan>
   <tspan x="1" y="100">and this is basically one sentence.</tspan>
  </text>
  <rect x="0" y="0" width="350" height="200" fill="none" stroke="blue" stroke-width="2"/>
</svg>

Now let’s try textLength on the <tspan> elements:

<svg width="350" height="200" viewBox="0 0 350 200">
 <text>
   <tspan x="1" y="80" textLength="350">I am a single line of text inside an SVG</tspan>
   <tspan x="1" y="100" textLength="350">and this is basically one sentence.</tspan>
 </text>
</svg>
Chrome, Safari & IE 11 Firefox

textLength does not work on <tspan> in Firefox. Results are even crazier if textLength is applied to <text> only:

 <svg width="350" height="200" viewBox="0 0 350 200">
  <text textLength="350">
   <tspan x="1" y="80">I am a single line of text inside an SVG</tspan>
   <tspan x="1" y="100">and this is basically one sentence.</tspan>
 </text>
</svg>
Chrome & Safari IE11 Firefox

Sigh. If textLength worked in Firefox like its spec says it does, this would be the end of the blog post. Hint hint, Mozilla.

Lessons learned:

  • textLength does not consistently work on <tspan> across browsers
  • Applying textLength to a text element with tspan elements inside gives highly erratic results
  • Firefox is making me sad

dx to the rescue (kind of)

I was about to give up on using text elements inside these SVGs because Firefox can’t seem to pass muster, when I came across dx.

The dx attribute indicates a shift along the x-axis on the position of an element or its content. . . .

For <text> & <tspan> … if a single <length> is provided, this value represents the new relative x coordinate for the current text position for the first character within this element or any of its descendants. The current text position is shifted along the x-axis of the current user coordinate system by<length> before the first character is rendered.

If a comma- or space-separated list of n <length>s is provided, then the values represent incremental shifts along the x-axis for the current text position of the first n characters.

Translation: you can scootch characters along the line individually.

<svg width="350" height="200" viewBox="0 0 350 200">
   <text>
    <tspan x="1" y="80" dx="0 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1">I am a single line of text inside an SVG</tspan>
    <tspan x="1" y="100" dx="0 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 2.2 ">and this is basically one sentence.</tspan>
   </text>
</svg>

Woohoo! Tedious, but woohoo!

Notice how the first value in each dx attribute is 0. The first value effects the first character, so 0 tells it not to move anywhere. You can also think of it like, do you want space before the character? N0. Each subsequent value effects the corresponding characters including spaces. So, count up your characters with spaces, punch in that number of <length> values and make the first one 0. (This is where something like Emmet comes in handy, where I can type {1 }*39, hit tab, and return 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1.)

Now, there are some caveats to this method:

  1. I don’t know a surefire way to figure out what the <length> values should be, so it’s trial and error as far as I know.
  2. Hard to see in the above example, but the browsers calc what the values actually mean a little differently than the each other:
    Chrome & Firefox IE 11 & Safari

    Chrome and Firefox calc dx values about the same

    IE and Safari calc dx values slightly wider than others

So if you use this method in production, I recommend deving for Chrome (as that has the widest usage) and setting overflow: hidden so that any hanging characters in Safari won’t get clipped (remember, IE is already visible by default).

A word on white space

SVGs are XML documents and XML documents handle white space a certain way. So if I have this:

<text>
 <tspan x="0" y="10">This is one line but</tspan>
 <tspan x="0" y="30">this is another.</tspan>
</text>

versus this:

<text>
 <tspan x="0" y="10">This is one line but</tspan> <tspan x="0" y="30">this is another.</tspan>
</text>

What do you think the difference in computed white space would be? Well without any special declarations, nothing. If you were to copy-paste the text into Notepad, you’d get one line: “This is one line but this is another.” Cool.

Now, for some reason and in some cases, and I can’t figure out when or why, Safari needs a white space declaration, otherwise lines will start and end in the wrong place and on the wrong lines. Here’s an example as a PNG so you can see it in case you don’t have Safari, but if you do you can check the pen here:

<svg version="1.1" viewBox="0 0 550 195" class="svg-text" role="heading">
  <text>
    <tspan x="2" y="15" font-size="18" dx="0 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 4.8 ">The development of a cure requires</tspan>
    <tspan x="0" y="115" font-size="118" dx="0 35 35 35 35">Time &amp; </tspan>
    <tspan x="0" y="190" font-size="73.5" dx="0 33 33 33 33 33 33">Funding</tspan>
  </text>
</svg>

Christ.

Other browsers print this out gravy:

There is one fail safe way to make sure your <tspan> lines line up where you want them to on all browsers, visually, and that is to declare xml:space="preserve" in <text> elements and everything will be cool. This does have the effect of, well, preserving white space, so copy and pasted the output will be
“The development of a cure requires    time &    funding.”

Tying it all together

Here is an example of how I actually used this in production:

See the Pen svg text by nicole (@namurray) on CodePen.0

  • The class svg-text takes care of font declarations, but you can just as easily inline them
  • role="heading" lets screen readers know this is a heading
  • Each <tspan> line has its own font-size and dx values
  • I even scooched the first line in a bit using the x value, just for aesthetic reasons
  • The last y value isn’t all the way at the bottom, bit up a bit, to account for a bit of font descension
  • A backup png is included in the svg itself, along with alt text

Leave a Reply

Your email address will not be published. Required fields are marked *

Back to Top