C is for Cookie, That's Good Enough For Me! The text is not part of an image. Hover over the image and see it animate. (Photo Credit: Renee Comet. Photo from Wikimedia Commons)

As a semi–retired calligrapher, I have always been obessesed with typography on-line. Typography on the web has improved immensely since I started doing web-development in the 1990’s, but passing by this poster at work for the last two years has inspired me into researching textpaths in HTML5. Designers have been using textpaths for years in print, but doing this on the web is a little tricky. SVG, which supports textpaths in modern browsers, is not supported by IE <= 8. Older IE does, however, support VML, another vector markup format that can do textpaths as well. After playing around, I found it is possible to put both on a web page and even code common CSS to style them — all without JavaScript. JavaScript can, however, be added to add some interesting animation effects, as these demos illustrate:

I’ll explain how we can layout and style textpaths in all browsers, including IE <= 8. I also explain what a Bezier Curve is, and introduce a tool, the Bezier Curve Construction Set, that will help you create your own scriptless textpaths. The only script you’ll need is when we cover animated textpaths, where I will show how I push text on a path as well tween a textpath. Note that all the demos and code in this article use jQuery, but there is no reason why you couldn’t use any other framework (or no framework at all) to do the same thing.

How to do this in modern browsers

Let’s walk through the SVG code that generates the curve that you see in the example above:

<svg id="myShape" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <path id="path1" fill="none" stroke="black" stroke-width="1" d="M 212,65 C 276,81 292,91 305,103 361,155 363,245 311,302 300,314 286,324 271,332 248,343 227,347 202,347 190,346 174,343 163,339 143,333" > </path> </defs> <text id="myText"> <textPath xlink:href="#path1" > <tspan dy="0.3em" >C is for Cookie, That's Good Enough For Me!</tspan> </textPath> </text> </svg>

SVG is XML-based markup that describes an image — an browser that supports it can read this description and draws it on the fly. In the markup above — the <defs> section contains the <path> (called path1 ) that we will use to draw the textpath with (you can see it referencing #path1 in the textpath ‘s xlink:href attribute). This path is described in the d attribute. If you are not familiar with SVG, you are probably feeling a little intimidated by this jumble of letters and numbers. Let me walk you through what each part means:

The M (the Move command tells the browser to move it’s virtual pen at the co-ordinate that follows it (in this case 212,65 . This wil be the starting point of our curve The C (the Cubic Bezier Curve command tells the computer to draw a path described by each group of six numbers that follow it (I’ll explain what these numbers mean in the next section).

Paths can also contain other commands like L (the Line-to command) which can draw a line from the last coordinate from the previous to it’s own co-ordinate. There are more commands available, but we will concentrate on these three commands in this article (for more information about all the SVG path commands, check out Path Element section of the SVG 1.1 Specification).

Also note the dy="0.3em" in the <tspan> tag. By default, text will sit on top of a SVG textpath curve. Setting dy="0.3em" will make the textpath go through the text instead. Later in this article, you will see that this is an important point if you are creating textpaths that will look consistant in IE <= 8..

So, what the heck is a Cubic Bezier Curve?

A Bezier Curve is a numeric representation of a curve and is commonly used in computer graphics. Bezier Curves were famously used in 1962 by the French engineer Pierre Bezier to design automobile bodies at Renault (although he wasn’t the first to use them for automotive design or come up with the math behind them … then again, a lot of people in history got credit for things they didn’t originally invent).

To describe what the numbers mean, let’s take a simple example of M 200, 300 C 100, 100, 500, 100, 400, 300 :

The browser starts the curve at (200, 300) (which is the co-ordinate in the M, or “Move” command) and ends the curve at (400, 300) (the last co-ordinate in the C, or “Cubic Curve” command). The first and second co-ordinates in the C command ( (100, 100) and (500, 100) ) are called control points. The computer draws the beginning of the curve as if the line is being drawn towards the first control point and at the end of the curve looks like it is being drawn from the last control point. This concept may be a little difficult to understand at first, so let’s take a look at an interactive version of the diagram to solidify your understanding of this concept. Drag and drop any of the points in it, and see how the path syntax changes at the bottom of the frame:

Now I understand this SVG path notation may be hard to grasp (it took me a while to), so I wrote a tool to help, the Bezier Curve Construction Set (it is actually what powers the interactive diagram above). You can use this tool to play around with Bezier Curves, and even include a background image that you may want use to guide you. I made all the demos in this article with the help of this tool.

See the Bezier Curve Construction Set In Action.

So, What About IE?

While IE9+ can render the SVG above, there are, unfortunately, still users of IE8 and lower. The good news is that these browsers can render VML, another Vector Markup Language (gee, maybe that’s what it stands for ;-) ). VML was submitted as a web standard in 1998, but wasn’t accepted. However, the W3C took bits of VML and competing vector format, PGML and came up with SVG. Because of this heritage, VML’s path syntax is very similar to SVG’s — although some of the commands differ, both use the M , C and L commands. The rest of the markup, however, is quite different:

<?import namespace="v" urn="urn:schemas-microsoft-com:vml" implementation="#default#VML" declareNamespace /> <v:group id="myShape" > <v:shape id="path1" allowoverlap="true" coordsize="407,407" filled="t" fillcolor="black" stroked="f" path="M 212,65 C 276,81 292,91 305,103 361,155 363,245 311,302 300,314 286,324 271,332 248,343 227,347 202,347 190,346 174,343 163,339" > <v:path textpathok="t"></v:path> <v:textpath id="myText" style="v-text-align: left" on ="t" string="C is for Cookie, That's Good Enough For Me!"></v:textpath> </v:shape> </v:group>

So, how do we make this markup work in all browsers? Conditional Comments to the rescue!

<!-- This first bit will be hidden from IE8 and below. All other user agents will render it. --> <!--[if IE 9 | !IE]><!--> <svg id="myShape" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <path id="path1" d="M 212,65 C 276,81 292,91 305,103 361,155 363,245 311,302 300,314 286,324 271,332 248,343 227,347 202,347 190,346 174,343 163,339 " > </path> </defs> <text id="myText"> <textPath xlink:href="#path1"> <tspan dy="0.3em">C is for Cookie, That's Good Enough For Me!</tspan> </textPath> </text> </svg> <!--<![endif]--> <!-- Only IE8 and below will see and render the VML markup below. --> <!--[if lt IE 9 ]> <?import namespace="v" urn="urn:schemas-microsoft-com:vml" implementation="#default#VML" declareNamespace /> <v:group id="myShape" > <v:shape id="path1" allowoverlap="true" coordsize="407,407" filled="t" fillcolor="black" stroked="f" path="M 212,65 C 276,81 292,91 305,103 361,155 363,245 311,302 300,314 286,324 271,332 248,343 227,347 202,347 190,346 174,343 163,339" > <v:path textpathok="t"></v:path> <v:textpath id="myText" style="v-text-align: left" on ="t" string="C is for Cookie, That's Good Enough For Me!"></v:textpath> </v:shape> </v:group> <![endif]-->

In the <head> , I also add the vml.css style-sheet, which also acts as a CSS reset for all the examples in this article.

I mentioned earlier that the SVG version needed the dy="0.3em" in the <tspan> tag to ensure parity with IE <= 8. Let us show you some screenshots showing where the path is relative to the text in both browsers:

IE <= 8 Chrome 26 (without dy=”0.3″) Chrome 26 (with dy=”0.3″)

See the demo where I got these screenshots from.

I actually like the SVG default behaviour of placing the text on top of the path, but there I don’t believe there is any way for IE to do this. :-/

Can I Use CSS With SVG and VML?

Absolutely! As a matter of fact, there are several CSS properties that are unique to both technologies. Let’s walk through the CSS of the example at the top of the page:

/*********************************************** * Note that I don't use the SVG or VML node * names in my CSS. I try to style the common * elements by using IDs on the similar nodes. ***********************************************/ /* * This rule is for the root of the SVG and VML * (i.e. the whole shape). */ #myShape { width: 407px; height: 407px; cursor: pointer; /* SVG */ /* * Webkit is the only browser that makes the transform * origin 0 0 (WHY?!?!). We also can't use 50% 50%, * because for SVG Webkit doesn't understand that * either (WHY!??!) */ -webkit-transform-origin: 203.5px 203.5px; /* * Even though it doesn't really do anything, * we do -webkit-transform here. The translateZ(0) * turns on GPU acceleration for animation on the * node (if available). I put the rotate(0deg) * here as since we need to have the same tranform * functions for all the animation states, otherwise * the transition will fail. */ -webkit-transform: translateZ(0) rotate(0deg); /* * We animate on hover for browsers that do CSS transitions. * The */ -o-transition: -o-transform 1s ease-out; -webkit-transition: -webkit-transform 1s ease-out; transition: transform 1s ease-out; /* * VML: a rotation in VML is surprisingly simple, and * doesn't need the horrible Matrix filter notation (yay!) */ rotation: 0; /* * This is needed by the VML in IE < 8 in order to grab * the mouseover events that do the animation in JavaScript */ background: url('../images/transparent.gif'); } /* IE9 will put the curve underneath the cookie unless we * set positioning to the <SVG> tag. We don't do this in * VML since positioning a <v:group> will mess up layout. */ svg#myShape { position: relative; } /* * This rule is to style the text (i.e. the <text> node in SVG * and the <v:shape> node in VML). */ #myText { font-size: 50px; /* SVG only. For VML, the fill happens in the <v:shape> tag. */ fill: black; /* Note: VML will not accept @font-face fonts (!!!) */ font-family: "Georgia", "Times New Roman", sans-serif; font-style: italic; } /* * On hover, we want the shape animate in a full circle. * Note we don't bother with -ms-transform since this * is going to be handled by JavaScript, due to IE9's * support of CSS transitions (IE10, which does support * transitions, doesn't use the -ms vendor prefix with * either transforms or transitions). */ #myShape:hover { -o-transform: rotate(360deg); -webkit-transform: translateZ(0) rotate(360deg); transform: translateZ(0) rotate(360deg); /* * You would think this would work, but it doesn't, * even without the transition. (I leave it here for * to call this fact out). Seems like we can't * rotate in VML on hover, so we do this with * JavaScript instead via the mouseover * event. */ rotation: 45; } #cookie { left: -4px; position: absolute; top: 0; }

The interesting bits:

The fill: black <text> tag instead of using color to set the text’s color. This is because SVG thinks of all objects, including text, as shapes, which can have independent stroke (i.e. outline) and fill styles. You will note that for the VML, we must set filled="t" and fillcolor="black" inside the <v:shape> tag (It’s a shame this can’t be done in CSS).

tag instead of using to set the text’s color. This is because SVG thinks of all objects, including text, as shapes, which can have independent (i.e. outline) and styles. You will note that for the VML, we must set and inside the tag (It’s a shame this can’t be done in CSS). While modern browsers can use variations of the transform property, the really surprising thing here is the rotation instead of using the really ugly numbers in the Matrix Filter. The problem here is that you cannot set a different value for this property using a :hover pseudo-class (Why IE?!?! WHY!?!?? It’s as if the people who built the IE back in the day hated web developers!).

property, the really surprising thing here is the instead of using the really ugly numbers in the Matrix Filter. The problem here is that you cannot set a different value for this property using a pseudo-class (Why IE?!?! WHY!?!?? It’s as if the people who built the IE back in the day hated web developers!). Note that I avoid using selectors with tag names, due to the huge difference between VML and SVG. Instead I put the same id’s on the VML and SVG tags that render the same thing (e.g. #myText for styling the text in the vector markup, #myShape for styling the width and height of the vector markup’s canvas, etc).

For those interested, Opera has a list of SVG-specific CSS properties and which ones it currently supports, and the VML submission to the W3C has a section on CSS for VML.

This CSS works well in IE <= 9 but the animations don't, so we include this script for these challenged browsers:

/* * This implements the rotation animation in * IE <= 9, using jQuery.animate() on VML's * rotation CSS property (for IE <= 8) and * IE9's -ms-transform CSS property. */ var IEAnimation = new function () { var me = this, $myShape; me.init = function () { $myShape = $('#myShape'); $myShape.hover(hoverIn, hoverOut); } function hoverIn(e) { // rotate from 0 to 360 in 1000 millseconds. $myShape.stop().animate({ // This animated VML's rotation CSS property // for IE <= 8 rotation: 360 }, { duration: 1000, step: function (now, tween) { // We need to animation IE9's -ms-transform property // in the step method, since it is non-numeric. now // will be a number of degrees given in the rotation // property. Even though that property doesn't exist // in IE9 and above, animate() still uses it to // populate the step method's now parameter. $myShape.css('msTransform', 'rotate(' + now + 'deg)'); } }); } function hoverOut(e) { // rotate from 360 to 0 in 1000 millseconds. Same // idea as the hoverIn() method. $myShape.stop().animate({ rotation: 0 }, { duration: 1000, step: function (now, tween) { $myShape.css('msTransform', 'rotate(' + now + 'deg)'); } }); } } $(document).ready(IEAnimation.init)

Other Types of Animations.

Animating Text Along An Arbitrary Path.

Click on the screenshot below to see text animate along an image of a roller-coaster:



The SVG is rather similar to the “Cookie” example above, but let me highlight a few points:

<!--[if IE 9 | !IE]><!--> <svg id="myShape" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" > <defs> <!-- This is a path with two curves fused together --> <path id="path1" fill="none" stroke="black" stroke-width="1" d="M 1, 281 C 479, 74, 502, 65, 779, 200 834, 236, 915, 343, 1023, 468"> </path> </defs> <text id="myText"> <textpath class="textpath" xlink:href="#path1" startOffset="0%"> <tspan dy="0.3em">Type Doesn't Have To Be Flat!</tspan> </textpath> </text> </svg> <!--<![endif]--> <!--[if lt IE 9 ]> <?import namespace="v" urn="urn:schemas-microsoft-com:vml" implementation="#default#VML" declareNamespace /> <v:group id="myShape" > <!-- This is a path with two curves fused together --> <v:shape id="path1" allowoverlap="true" coordsize="1024,481" filled="t" fillcolor="black" stroked="f" path="M 1, 281 C 479, 74, 502, 65, 779, 200 834, 236, 915, 343, 1023, 468 "> <v:path textpathok="t" /> <v:textpath id="myText" on="t" string="Type Doesn't Have To Be Flat!" /> </v:shape> </v:group> <![endif]-->

Note that take the <textpath> node and set its class to "textpath" . This is because I cannot use jQuery to search for SVG nodeNames (e.g. $('textpath') ), but I can search for classes of SVG nodes (e.g. $('.textpath') ).

Let’s also take a look at where the text appears in each format. In SVG, it appears as text inside inside the <tspan> node, while in VML, it appears inside the <v:textpath> ‘s string attribute. This is the same as the previous example, but this difference comes into play when we are animate the text along it’s path using jQuery.animate() . In SVG browsers, we manipulate the <textPath> node’s startOffset attribute, increasing it from 0% to 100% . For VML browsers, we have to add spaces at the beginning of the <v:textpath> node’s string attribute. Here is a simplified version of the actual JavaScript code.

var originalText = "Text Doesn't Have To Be Flat!"; $textpath = $('textPath'); // note Chrome doesn't like 'textpath' with a lower case 'p'. /* * Since we want $textpath to be set to the SVG node, not the VML node, * we check the parentNode to make sure. */ if ($textpath[0].parentNode.nodeName != 'text') { $textpath = []; } $('#hoverControl").hover(hoverIn, hoverOut); function hoverIn(e) { /* * We are animting using a dummy CSS property, * appropriately called 'dummy'. :-) */ $myShape.stop().animate({ dummy: 100 }, { duration: duration, /* * now is going to be between 0 and 100. */ step: function (now, tween) { // For SVG browsers, we modify the startOffset attribute to push the text if ($('svg textPath').length > 0) { $textpath[0].setAttribute('startOffset', now + '%'); // In IE <= 8, we have to add spaces to the beginning of the original // test on the path. :-/ } else { ('#myText').string = getSpaces(now) + originalText; } } }); } /* * getSpaces(): returns a string with n space in it. */ function getSpaces(n) { var r = ''; for (var i=0; i

The above is just a partial listing of the demo source. The full source is available here at GitHub.

See the roller-coaster demo in action.

Tweening the Text Path Itself

To illustrate how to tween on the path itself, let's use this example of text wrapping around a martini glass:

<!--[if IE 9 | !IE]><!--> <svg id="myShape" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"> <defs> <filter id="drop-shadow"> <feGaussianBlur in="SourceAlpha" stdDeviation="3" result="blurred"/> <feOffset in="blurred" dx="0" dy="0" result="final_blur"/> <feMerge> <feMergeNode in="final_blur"/> <feMergeNode in="SourceGraphic"/> </feMerge> </filter> <path id="headerPath" fill="none" stroke="white" stroke-width="1" d="M 27, 143 C 190, 111, 378, 122, 508, 148" > </path> <path id="leftGlassPath" fill="none" stroke="white" stroke-width="1" d="M 5, 182 C 201, 479, 233, 478, 232, 529 L 232, 732" > </path> <path id="rightGlassPath" fill="none" stroke="white" stroke-width="1" d="M 318, 735 L 318, 530 C 317, 478, 329, 478, 531, 183" > </path> <path id="bottomGlassPath" fill="none" stroke="white" stroke-width="1" d="M 153, 831 C 215, 894, 372, 878, 398, 821" > </path> </defs> <text id="headerText" > <textPath class="textpath" xlink:href="#headerPath" startOffset="50%" > <tspan dy="0.3em">A Perfect Martini!</tspan></textPath> </text> <text id="leftGlassText" > <textPath class="textpath" xlink:href="#leftGlassPath" style="filter:url(#drop-shadow)" startOffset="50%" > <tspan dy="0.3em"> A hint of dry vermouth, two shots of gin and stirred — never, ever shaken. </tspan> </textPath> </text> <text id="rightGlassText" > <textPath class="textpath" xlink:href="#rightGlassPath" style="filter:url(#drop-shadow)" startOffset="50%" > <tspan dy="0.3em"> Garnished with a blue cheese olive, a pickled onion, or a twist of lemon. </tspan> </textPath> </text> <text id="bottomGlassText"> <textPath class="textpath" xlink:href="#bottomGlassPath" style="filter:url(#drop-shadow)" startOffset="50%" > <tspan dy="0.3em">A drink of refinement and class.</tspan> </textPath> </text> </svg> <!--<![endif]--> <!--[if lt IE 9 ]> <?import namespace="v" urn="urn:schemas-microsoft-com:vml" implementation="#default#VML" declareNamespace /> <v:group id="myShape" class="vml" > <v:shape id="headerPath" allowoverlap="true" coordsize="597,960" filled="t" fillcolor="white" stroked="f" path="M 27, 143 C 190, 111, 378, 122, 508, 148"> <v:path textpathok="t" /> <v:textpath id="headerText" on="t" string="A Perfect Martini!" /> </v:shape> <v:shape id="leftGlassPath" allowoverlap="true" coordsize="597,960" filled="t" stroked="f" path="M 5, 182 C 201, 479, 233, 478, 232, 529 L 232, 732"> <v:path textpathok="t" /> <v:textpath id="leftGlassText" on="t" string="A hint of dry vermouth, two shots of gin and stirred — never, ever shaken." /> <v:shadow id="shadow2" on="True" color="#000000" offset="2px,-2px" obscured="True" opacity="50%"/> </v:shape> <v:shape id="rightGlassPath" allowoverlap="true" coordsize="597,960" filled="t" stroked="f" path="M 318, 735 L 318, 530 C 317, 478, 329, 478, 531, 183 "> <v:path textpathok="t" /> <v:textpath id="rightGlassText" on="t" string="Garnished with a blue cheese olive, a pickled onion, or a twist of lemon." /> <v:shadow id="shadow2" on="True" color="#000000" offset="2px,-2px" obscured="True" opacity="50%"/> </v:shape> <v:shape id="bottomGlassPath" allowoverlap="true" coordsize="597,960" filled="t" stroked="f" path="M 153, 831 C 215, 894, 372, 878, 398, 821"> <v:path textpathok="t" /> <v:textpath id="bottomGlassText" on="t" string="A drink of refinement and class." /> <v:shadow id="shadow3" on="True" color="#000000" offset="2px,2px" obscured="True" opacity="50%"/> </v:shape> </v:group> <![endif]-->

Before we get into the animation code, let's look at the markup that produces the shadow behind the text. Unfortunately, text-shadowing in most browsers is not as straight-forward as using the text-shadow CSS property. While it works in Chrome and Safari, it doesn't in Firefox, Opera or IE. The closest cross-browser solution for SVG is to use an SVG Filter (SVG filters are a whole topic itself — a great place to start to learn is thge W3C SVG primer on Filters). Firefox, Chrome, and Safari support SVG filters. IE uses the shadow tag. Unforunately, there is no drop-shadowing supported in Opera. You will also note that these text-shadows are not consistant (the SVG filter I included is a blurred one, while the IE one is not). There are several challenges like this when dealing with some of the fancier styling you may want to do with SVG and VML text.

As for the animation, each of the VML/SVG nodes that have a path in it also has a data-end-d attribute to it. The data-end-d does not do anything nativelty in VML or SVG — I use JavaScript to tween the animation of the curve to the value in this attribute. I have posted this code (as well as all the code in this article) in GitHub, so invite you to walk through the code yourself to see how this works (I hope the comments are self-explanatory). One thing to note: when tweening SVG paths in JavaScript, you must set the textpath's xlink:href attribute in the text-path to the same path in order to force a repaint in older WebKit browsers (I was wondering why the iPad I was using wouldn't tween the martini example until I found Mike Bostock's workaround for this issue).

If you view the martini example in IE <= 8, you'll see that VML cannot embed fonts using @font-face . If this is a show-stopper for you, then you are out of luck. However, you can degrade gracefully to a system font if it's not, and I believe that neat animation effect may outweigh the lack of @font-face support in general coolness. :-)

Internet Explorer <= 8 Firefox 21 (other browsers, including IE 9+, look similar)

See the above example in action.

In Conclusion.

As I mentioned throughout this article, there are a few gotchas that you have to keep in mind when doing cross-browser textpaths:

You can use CSS to style a few things (e.g. font-families, -weights and -sizes). Other things you can style in CSS in SVG only and with VML node attributes (e.g. fill and stroke colors and widths). Still other things require SVG filters and additional VML tags (e.g. text-shadows). Both SVG and VML have their own unique CSS properties (and some of the SVG ones are not supported by all browsers). VML doesn't support @font-face (KHHHHAAAAAANNNN!!!!). jQuery cannot do cross-browser searches for SVG nodes by tag-name, but you can search for nodes via classes and ids. When tweening SVG paths in JavaScript, you must set the textpath's xlink:href attribute in the text-path to the same path in order to force a repaint in older WebKit browsers. Older-IE can rotate VML objects a lot more easily than HTML objects.

Despite the challenges, I believe this opens up a lot of typographic possibilities on the web. There is admittedly quite a little bit of work to set up right the first time you do it, but I've found it's easy to do once you have the basic template down and know the gotchas listed in this article. Feel free to download all the code in this article and play. It'll be another interesting trick in your web development toolkit to show-off with.

Download all the examples from GitHub.