Having fun with SVG text

February 21, 2024

Last weekend, I redesigned my blog. As part of this redesign, I wanted to make something fun for my front page. I started sketching a bit on paper, got an idea, and soon I was doing a search for "css curved text".

The first result I found was the Curved Text Along a Path tutorial by Geoff Graham on CSS Tricks. It demonstrates how to add curved text to a web page using an inline SVG.

It was perfect.

About half an hour of experimenting and browsing the MDN Web Docs later, I was looking at the end result. It's the most beautiful thing I've made in years.

My new front page, with links to my blog, webroll, Mastodon, and most prominently: a circular photo of a seal plush stuffed into a wine glass, with "Welcome to my website" written in balloon letters around the photo.
Pictured: My sweet little boy, trapped in glass.

Now, unless you have an above-average interest in HTML, CSS and vector graphics, you may want to tap out of this page now, because the rest of this will be me nerding out over how the sausage is made.

...

Still here?

Good! Let's jump in.

Where we're going, we don't need Photoshop

<header>
    <svg viewBox="0 0 500 500" xmlns="http://www.w3.org/2000/svg">
        <title>Welcome to my website</title>
        <path id="top-curve" d="M 100 100 C 100 90, 250 -40, 400 100" stroke="transparent" fill="transparent"/>
        <path id="bottom-curve" d="M 100 400 C 100 410, 250 540, 400 400" stroke="transparent" fill="transparent"/>
        <text aria-hidden="true" textLength="340">
            <textPath xlink:href="#top-curve">Welcome to</textPath>
        </text>
        <text aria-hidden="true" textLength="340">
            <textPath xlink:href="#bottom-curve" dominant-baseline="hanging">my website</textPath>
        </text>
    </svg>
    <img src="/img/logo-500px.jpg" alt="A seal in a wine glass">
</header>

This is the complete HTML for the front page header image and text. It is made up of two main elements:

A side note on screen readers

The word "draw" can be a bit misleading here, because it may lead you to believe the text is purely graphical.

Me selecting and highlighting part of the "Welcome to" text.
This brings me a silly amount of joy.

Yes - the SVG text functions pretty much like regular text! You can select it, copy it, and screen readers will even read it!

That last point is a problem. When testing how the SVG reads with the built-in screen reader on Windows 11, it was... bad. It just does not read well. It may be that other screen readers handle SVG text better, but I do not want to rely on it.

To improve on this, I went with the most robust-looking solution I could find: I added a <title> element with equivalent text to the SVG, and hid the text elements from screen readers with aria-hidden="true". This made the reading experience significantly more pleasant.

(If you know of better, more accessible ways to do this, please contact me so I can update this post.)

Styling

To reliably wrap the text around the photo, I needed some reliable styling, and I needed that styling to be responsive across different screen sizes.

I solved this using a CSS grid where:

.home-page header {
    display: grid;
    margin: 40px 0px;
    max-width: 500px; /* Will shrink, but keep height proportional if the screen is narrow */
}

.home-page header * {
    /* Place all child elements in the same, single cell of the grid */
    grid-column: 1;
    grid-row: 1;
    /* Ensure identical height and width */
    height: 100%;
    width: 100%;
}

.home-page header img {
    z-index: 1;
    border-radius: 100%; /* Makes the image circular */
    padding: 10%; /* Clears space for text around the image */
}

.home-page header svg {
    z-index: 2;
}

The image is made square by, well, being a square image. The SVG is made square by the viewBox="0 0 500 500" property, which defines the internal dimensions of the SVG - the draw positions range from (0,0) to (500,500).

And while we're in CSS world - the actual SVG text is also styled using CSS!

.home-page header svg text {
    fill: #89cff0;
    font-family: babycakes; /* Balloon font! https://www.fontspace.com/babycakes-font-f20531 */
    font-size: 36px;
    text-transform: uppercase;
}

Text, paths, and text paths

Let's look at the contents of the <svg> again, and remove <title> and aria-hidden for brevity.

<path id="top-curve" d="M 100 100 C 100 90, 250 -40, 400 100" stroke="transparent" fill="transparent"/>
<path id="bottom-curve" d="M 100 400 C 100 410, 250 540, 400 400" stroke="transparent" fill="transparent"/>
<text textLength="340">
    <textPath xlink:href="#top-curve">Welcome to</textPath>
</text>
<text textLength="340">
    <textPath xlink:href="#bottom-curve" dominant-baseline="hanging">my website</textPath>
</text>

The <text> elements draw text. The textLength attributes say how many pixels long the text should be, and squishes or stretches the text to make it so. I gave both the top and bottom text elements textLength="340" so they would stretch evenly left-to-right.

Inside them are the <textPath> elements, which draw text along a given path. The xlink:href attributes say which path they should follow. The dominant-baseline="hanging" attribute places the bottom text below the path, instead of above it.

Finally, the transparent <path> elements supply the curves, as defined by their d attribute.

Can we talk about the curves? I need to talk to you about the curves.

Bézier, you beautiful man

Okay, so I've been in the rough vicinity of graphics and animation long enough to have heard "Bézier curves" referenced a few dozen times in my life. I never took the time to look up what they were. I assumed they were complicated and slightly magical, only taught in spellbooks sourced from the Graphics Wizards' tower.

To draw my text exactly how I wanted it, I needed Bézier curves. So I had to up look how they worked, and what all of the coordinates in the code samples meant.

It turns out I was bang on the money. They are magical.

Animation of a quadratic (three control point) Bézier curve
Ah, Bézier, you've done it again!

I cannot hope to give a servicable tutorial for how they work - try the Bézier curve tutorial and SVG Paths tutorial on MDN for that.

In short, they consist of some number of control points, and move from the first control point to the final control point via the fun math-y thing the animation demonstrates. Two control points makes a linear curve (a line), three makes a quadratic curve, four makes a cubic curve, and so on.

This gave me just enough to go on to make a symmetric pair of cubic curves that gently curve around the image.

<!-- Cubic curve from (100,100), via (100,90) and (250,-40), to (400,100) -->
<path id="top-curve" d="M 100 100 C 100 90, 250 -40, 400 100"/>

Now, uh. In the middle of writing this, while reading the linked SVG Paths tutorial, I realized I could have made this using a simpler quadratic curve instead.

<!-- Quadratic curve from (100,95), via (250,-25), to (400,94) -->
<path id="top-curve" d="M 100 95 Q 250 -25, 400 95"/>

And. I also realized there's a non-Bézier "arc" curve. For drawing arcs. Like around a circle.

<!-- Arc from (100,100), in a 210 px radius with sweep-flag enabled, to (400,100) -->
<path id="top-curve" d="M 100 100 A 210 210 0 0 1 400 100"/>

It turns out arcs are a little bit complicated so for this use case I'm happy keeping it a Bézier curve.

Though... throwing a couple of arcs together is a simple way to draw a full circle path... so maybe...

I've already written up everything I learned so far, so let's close on this.

<path id="curve" d="M 100 100 A 210 210 0 0 1 400 400 A 210 210 0 0 1 100 100" stroke="transparent" fill="transparent"/>
<text aria-hidden="true" textLength="1320">
    <textPath xlink:href="#curve">
        Imperial futures are only ever stolen presents.
    </textPath>
</text>
My front page image, but now in a full circle around the photo it says "Imperial futures are only ever stolen presents."
That's all, folks!